From 437cd94e1b7227910bee3aa6042c5b789f4dde2c Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 03:15:11 +0000 Subject: [PATCH 01/38] =?UTF-8?q?=E6=96=87=E6=A1=A3:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20FSM=20=E5=89=8D=E7=AB=AF=E5=9B=BD=E9=99=85=E5=8C=96=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specs/2026-06-21-fsm-i18n-design.md | 735 ++++++++++++++++++ 1 file changed, 735 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-21-fsm-i18n-design.md diff --git a/docs/superpowers/specs/2026-06-21-fsm-i18n-design.md b/docs/superpowers/specs/2026-06-21-fsm-i18n-design.md new file mode 100644 index 00000000..ad0a2e45 --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-fsm-i18n-design.md @@ -0,0 +1,735 @@ +# FSM Frontend Internationalization (i18n) Design Spec + +**Date:** 2026-06-21 +**Project:** factorio-server-manager-joey +**Branch:** feat/fsm-i18n +**Status:** Draft / Approved for Implementation + +--- + +## 1. Overview and Goals + +### 1.1 Problem + +The Factorio Server Manager (FSM) frontend has zero internationalization support. All UI strings are hardcoded English embedded in JSX components. This blocks adoption by non-English-speaking server operators, particularly the Chinese community where Factorio has a large player base. + +### 1.2 Goals + +- Enable the UI to render in Simplified Chinese (zh-CN) as the first translated locale. +- Keep English as the authoritative fallback language (maintain existing strings as the source of truth). +- Introduce a translation architecture that makes adding future locales trivial (single new JSON folder). +- Minimize component refactoring: the `useTranslation()` Hook pattern mirrors existing React functional component style. +- Zero backend changes. Go error messages, log entries, and RCON output are not translated. +- Complete coverage of all ~200 UI strings across 9 view/page areas. + +### 1.3 Non-Goals (see also Section 10) + +- Translating Go backend strings, error messages, or Factorio server log output. +- Runtime locale switching without page reload (nice-to-have, not required for MVP). +- Right-to-left (RTL) language support. +- Pluralization rules or ICU MessageFormat. +- Integration with CI translation services (POEditor, Crowdin, etc.). + +--- + +## 2. Architecture + +### 2.1 Framework: react-i18next + i18next + +Chosen over alternatives (react-intl, LinguiJS) because: + +- `useTranslation()` Hook matches the existing functional component + Hooks pattern perfectly. +- No Redux or global state manager dependency (the app uses only `useState`). +- Backend-agnostic: translations are loaded as static JSON at build time, no XHR needed. +- `i18next-browser-languagedetector` provides zero-config language detection from `navigator.language`. + +### 2.2 Installation + +```bash +npm install i18next react-i18next i18next-browser-languagedetector +``` + +All three packages are runtime dependencies. No additional Webpack plugins or loaders are required (JSON imports are already supported by Webpack 5 with the default `json` module type; the existing config resolves `.json` extensions via `resolve.extensions`). + +### 2.3 Initialization + +A single initialization module is created at `ui/i18n.js`: + +```js +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import enCommon from './locales/en/common.json'; +import enLayout from './locales/en/layout.json'; +import enControls from './locales/en/controls.json'; +import enMods from './locales/en/mods.json'; +import enSaves from './locales/en/saves.json'; +import enServerSettings from './locales/en/serverSettings.json'; +import enLogs from './locales/en/logs.json'; +import enConsole from './locales/en/console.json'; +import enUserManagement from './locales/en/userManagement.json'; + +import zhCommon from './locales/zh-CN/common.json'; +import zhLayout from './locales/zh-CN/layout.json'; +import zhControls from './locales/zh-CN/controls.json'; +import zhMods from './locales/zh-CN/mods.json'; +import zhSaves from './locales/zh-CN/saves.json'; +import zhServerSettings from './locales/zh-CN/serverSettings.json'; +import zhLogs from './locales/zh-CN/logs.json'; +import zhConsole from './locales/zh-CN/console.json'; +import zhUserManagement from './locales/zh-CN/userManagement.json'; + +const resources = { + en: { + common: enCommon, + layout: enLayout, + controls: enControls, + mods: enMods, + saves: enSaves, + serverSettings: enServerSettings, + logs: enLogs, + console: enConsole, + userManagement: enUserManagement, + }, + 'zh-CN': { + common: zhCommon, + layout: zhLayout, + controls: zhControls, + mods: zhMods, + saves: zhSaves, + serverSettings: zhServerSettings, + logs: zhLogs, + console: zhConsole, + userManagement: zhUserManagement, + }, +}; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources, + fallbackLng: 'en', + defaultNS: 'common', + ns: [ + 'common', 'layout', 'controls', 'mods', 'saves', + 'serverSettings', 'logs', 'console', 'userManagement', + ], + interpolation: { + escapeValue: false, // React already escapes output + }, + detection: { + // Only check localStorage and navigator.language + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + lookupLocalStorage: 'fsm_lang', + }, + }); + +export default i18n; +``` + +### 2.4 Provider Wrapping + +In `ui/index.js`, import the i18n instance and wrap `` with ``: + +```jsx +import './i18n'; // initialize i18n before rendering +import React, { Suspense } from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App/App.jsx'; + +window.flash = (message, color = 'gray-light') => + Bus.emit('flash', ({ message, color })); + +const root = ReactDOM.createRoot(document.getElementById('app')); +root.render( + Loading...}> + + +); +``` + +`react-i18next` uses `Suspense` internally to delay rendering until translations are loaded. The `fallback` shows a brief loading indicator. For this app with static JSON imports, the Suspense resolves synchronously, so the fallback is a safety measure only. + +### 2.5 Translation File Structure + +``` +ui/locales/ + en/ + common.json + layout.json + controls.json + mods.json + saves.json + serverSettings.json + logs.json + console.json + userManagement.json + zh-CN/ + common.json + layout.json + controls.json + mods.json + saves.json + serverSettings.json + logs.json + console.json + userManagement.json +``` + +Every namespace folder mirrors `en/` exactly. Missing keys in `zh-CN/*.json` automatically fall back to `en/*.json` at runtime (no crash, no missing string). + +--- + +## 3. Component-Level i18n Integration Pattern + +### 3.1 Basic Pattern + +**Before** (hardcoded English): + +```jsx +import React from 'react'; +import Button from '../components/Button'; + +const Controls = ({ serverStatus }) => { + return ( + + ); +}; +``` + +**After** (translated): + +```jsx +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '../components/Button'; + +const Controls = ({ serverStatus }) => { + const { t } = useTranslation('controls'); + + return ( + + ); +}; +``` + +### 3.2 Component with Multiple Namespaces + +When a view imports strings from `common` and its own namespace: + +```jsx +const { t } = useTranslation(['mods', 'common']); +// Usage: +t('installMod') // looks in 'mods' first +t('save') // looks in 'common' (fallback namespace) +t('common:save') // explicit namespace prefix +``` + +Explicit prefix (`'common:save'`) is preferred for clarity when mixing namespaces, except in trivial cases. + +### 3.3 Interpolation (Dynamic Values) + +For strings that embed variables (save names, usernames, version numbers): + +**Translation JSON** (`en/common.json`): +```json +{ + "saveLastModified": "Last Modified: {{date}}", + "deleteConfirm": "Are you sure you want to delete \"{{name}}\"?", + "versionInfo": "Version {{version}}" +} +``` + +**Component usage:** +```jsx +{t('saveLastModified', { date: save.last_mod })} +{t('deleteConfirm', { name: save.name })} +{t('versionInfo', { version: factorioVersion })} +``` + +### 3.4 No Changes to Pure Presentational Components + +Components like `Button`, `Panel`, `Input`, `Label`, `Modal` do NOT import `useTranslation`. They accept `children` or `text` props. The translation call happens at the call site. + +**Exception:** `ConfirmDialog` receives translated `title`, `content` props from its parent. Its own button labels ("Cancel", "Confirm") are translated inside the component via `useTranslation('common')`. + +### 3.5 Changes Outside JSX + +For the `window.flash()` calls in JavaScript logic (e.g., error toasts): + +```jsx +import { useTranslation } from 'react-i18next'; +const { t } = useTranslation('common'); + +// Before: +window.flash("Login failed. Username or Password wrong.", "red"); + +// After: +window.flash(t('loginFailed'), "red"); +``` + +For non-component files (api resources, utility modules) that display user-facing messages, they must either accept a `t` function as a parameter or the calling component must wrap the message. + +--- + +## 4. Translation Key Naming Convention + +### 4.1 Rules + +- **Flat camelCase.** No dots, no nesting. Each JSON file is a single-level object. +- **VerbPrefix for actions.** `startServer`, `stopServer`, `deleteSave`, `uploadMod`. +- **NounOnly for labels.** `serverStatus`, `factorioVersion`, `lastModified`, `actions`. +- **Status constants in SCREAMING_SNAKE.** `RUNNING`, `STOPPED`, `UNKNOWN`. +- **Error messages:** `errorRequired`, `errorInvalidPort`, `loginFailed`. +- **Confirm dialogs:** `confirmDeleteMod`, `confirmStopServer`. +- **Placeholder text:** `placeholderUsername`, `placeholderPassword`. + +### 4.2 Examples per Category + +``` +# Controls +"startServer" "Start Server" +"saveStopServer" "Save & Stop Server" +"killServer" "Kill Server" +"serverStatus" "Server Status" +"status" "Status" +"ip" "IP" +"port" "Port" +"factorioVersion" "Factorio Version" +"save" "Save" +"RUNNING" "Running" +"STOPPED" "Stopped" +"UNKNOWN" "Unknown" + +# Common +"save" "Save" +"cancel" "Cancel" +"confirm" "Confirm" +"delete" "Delete" +"loading" "Loading..." +"errorOccurred" "An error occurred" +"loginFailed" "Login failed. Username or Password wrong." +"name" "Name" +"actions" "Actions" +``` + +### 4.3 What NOT to Do + +``` +# BAD - deep nesting +"server.controls.start" "Start Server" + +# BAD - inconsistent casing +"start_server" "Start Server" +"stopServer" "Stop Server" + +# BAD - HTML in values +"welcomeMessage" "Welcome to FSM" +``` + +Values are plain strings. No HTML markup inside JSON values. Components use React elements if formatting is needed. + +--- + +## 5. Language Detection and Switching UX + +### 5.1 Auto-Detection + +On first visit, `i18next-browser-languagedetector` checks `navigator.language`: + +| Browser Language | Selected Locale | +|---|---| +| `zh-CN`, `zh-TW`, `zh-HK`, `zh` | `zh-CN` | +| Any other value | `en` | + +The check uses simple prefix matching: if `navigator.language` starts with `zh`, load `zh-CN`. Otherwise, load `en`. + +### 5.2 Persistence + +The selected language is stored in `localStorage` key `fsm_lang`. On subsequent visits, the stored preference takes priority over `navigator.language`. + +Clearing `localStorage` or using a private/incognito window triggers auto-detection again. + +### 5.3 Language Switcher UI + +A language toggle is added to the Layout sidebar, inside the "FSM Administration" section, below the Help link: + +```jsx +// In Layout.jsx, after the Help link: +Users +Help +{/* new */} +
+ +
+``` + +The `` in FSM Administration section: + ```jsx + + ``` + +**Verification:** +- All sidebar strings render correctly +- Language switcher toggles between en/zh-CN immediately +- Language preference persists in localStorage +- `npm run build` succeeds + +--- + +### T7: Controls Page i18n + +**Status:** pending + +**File:** `ui/App/views/Controls.jsx` (MODIFY) + +**Changes:** +- Add `useTranslation('controls')` hook +- Replace panel title, field labels, button texts, error messages with `t()` calls +- Keep dynamic values (bindip, port, savefile) unchanged + +**Verification:** +- All hardcoded English strings replaced +- Dynamic values preserved +- `npm run build` succeeds + +--- + +### T8: Mods Page i18n (7 files) + +**Status:** pending + +**Files (all MODIFY):** +1. `ui/App/views/Mods/Mods.jsx` — panel titles, buttons, warning banner +2. `ui/App/views/Mods/components/UploadMod.jsx` — form labels, buttons +3. `ui/App/views/Mods/components/LoadMods.jsx` — form elements, ConfirmDialog, flash +4. `ui/App/views/Mods/components/ModPack.jsx` — ConfirmDialog usage +5. `ui/App/views/Mods/components/CreateModPack.jsx` — button, Modal title, form labels +6. `ui/App/views/Mods/components/AddMod/components/AddModForm.jsx` — form labels, buttons +7. `ui/App/views/Mods/components/AddMod/components/FactorioLogin.jsx` — form labels, button, flash +8. `ui/App/views/Mods/components/AddMod/components/SelectVersionForm.jsx` — Modal title, table headers + +**Verification:** +- All mods pages render with translated strings +- Tab navigation works +- ConfirmDialog shows translated buttons +- `npm run build` succeeds + +--- + +### T9: Saves Page i18n (3 files) + +**Status:** pending + +**Files (all MODIFY):** +1. `ui/App/views/Saves/Saves.jsx` — panel titles, table headers, disabled state +2. `ui/App/views/Saves/components/CreateSaveForm.jsx` — form labels, button, error +3. `ui/App/views/Saves/components/UploadSaveForm.jsx` — form label, placeholder, error, button + +**Verification:** +- All save pages render with translated strings +- Delete confirmation shows proper strings +- `npm run build` succeeds + +--- + +### T10: Settings + Console + Logs i18n (4 files) + +**Status:** pending + +**Files (all MODIFY):** +1. `ui/App/views/ServerSettings.jsx` — panel title, save button, flash message +2. `ui/App/views/GameSettings.jsx` — panel title +3. `ui/App/views/Console.jsx` — panel title, disabled message, input placeholder +4. `ui/App/views/Logs.jsx` — panel title + +**Verification:** +- All 4 pages render with translated strings +- Flash message uses translated string +- `npm run build` succeeds + +--- + +### T11: User Management + Login + Help i18n (5 files) + +**Status:** pending + +**Files (all MODIFY):** +1. `ui/App/views/UserManagement/UserManagment.jsx` — panel titles, table headers +2. `ui/App/views/UserManagement/components/CreateUserForm.jsx` — labels, placeholders, errors, buttons +3. `ui/App/views/UserManagement/components/ChangePasswordForm.jsx` — labels, placeholders, errors, buttons, flash +4. `ui/App/views/Login.jsx` — panel title, form labels, placeholders, errors, buttons, flash +5. `ui/App/views/Help.jsx` — panel title, section headings, body text + +**Verification:** +- All pages render with translated strings +- Flash messages use translated text +- Form validation errors show translated text +- `npm run build` succeeds + +--- + +### T12: Build Verification + +**Status:** pending + +**Command:** +```bash +npm run build +``` + +**Checks:** +- Build exits with code 0 +- No i18n-related warnings +- Output files exist in `app/` directory + +--- + +### T13: Atomic Commit + +**Status:** pending + +**Commit message:** `功能: 添加 FSM 前端国际化支持(zh-CN)` + +**Files staged:** ~30 files (new i18n config + 18 JSON locale files + ~12 modified components + package.json + package-lock.json) + +--- + +## Success Criteria + +1. `npm run build` exits 0 with no i18n-related warnings +2. All files modified/created as specified +3. English locale loads by default +4. Chinese locale auto-detects when `navigator.language = 'zh-CN'` +5. Language switcher toggles between en/zh-CN immediately +6. Language preference persists across page reload +7. Missing zh-CN keys fall back to English gracefully +8. All pages render without console errors in both languages From 6e452f0edbb2ab6129061ad70e70aa370fde0673 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 03:22:30 +0000 Subject: [PATCH 03/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20i18n=20=E6=A0=B8=E5=BF=83=E5=9F=BA=E7=A1=80=E8=AE=BE?= =?UTF-8?q?=E6=96=BD=20+=20=E8=8B=B1=E6=96=87=E7=BF=BB=E8=AF=91=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=889=20=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/i18n.js | 69 +++++++++++++++++++++++++++++++ ui/index.js | 9 +++- ui/locales/en/common.json | 42 +++++++++++++++++++ ui/locales/en/console.json | 8 ++++ ui/locales/en/controls.json | 20 +++++++++ ui/locales/en/layout.json | 21 ++++++++++ ui/locales/en/logs.json | 8 ++++ ui/locales/en/mods.json | 32 ++++++++++++++ ui/locales/en/saves.json | 13 ++++++ ui/locales/en/serverSettings.json | 30 ++++++++++++++ ui/locales/en/userManagement.json | 20 +++++++++ 11 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 ui/i18n.js create mode 100644 ui/locales/en/common.json create mode 100644 ui/locales/en/console.json create mode 100644 ui/locales/en/controls.json create mode 100644 ui/locales/en/layout.json create mode 100644 ui/locales/en/logs.json create mode 100644 ui/locales/en/mods.json create mode 100644 ui/locales/en/saves.json create mode 100644 ui/locales/en/serverSettings.json create mode 100644 ui/locales/en/userManagement.json diff --git a/ui/i18n.js b/ui/i18n.js new file mode 100644 index 00000000..d730fa9e --- /dev/null +++ b/ui/i18n.js @@ -0,0 +1,69 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +// English locale files (fallback language) +import enCommon from './locales/en/common.json'; +import enLayout from './locales/en/layout.json'; +import enControls from './locales/en/controls.json'; +import enMods from './locales/en/mods.json'; +import enSaves from './locales/en/saves.json'; +import enServerSettings from './locales/en/serverSettings.json'; +import enLogs from './locales/en/logs.json'; +import enConsole from './locales/en/console.json'; +import enUserManagement from './locales/en/userManagement.json'; + +// Chinese locale files +import zhCommon from './locales/zh-CN/common.json'; +import zhLayout from './locales/zh-CN/layout.json'; +import zhControls from './locales/zh-CN/controls.json'; +import zhMods from './locales/zh-CN/mods.json'; +import zhSaves from './locales/zh-CN/saves.json'; +import zhServerSettings from './locales/zh-CN/serverSettings.json'; +import zhLogs from './locales/zh-CN/logs.json'; +import zhConsole from './locales/zh-CN/console.json'; +import zhUserManagement from './locales/zh-CN/userManagement.json'; + +const resources = { + en: { + common: enCommon, + layout: enLayout, + controls: enControls, + mods: enMods, + saves: enSaves, + serverSettings: enServerSettings, + logs: enLogs, + console: enConsole, + userManagement: enUserManagement, + }, + 'zh-CN': { + common: zhCommon, + layout: zhLayout, + controls: zhControls, + mods: zhMods, + saves: zhSaves, + serverSettings: zhServerSettings, + logs: zhLogs, + console: zhConsole, + userManagement: zhUserManagement, + }, +}; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources, + fallbackLng: 'en', + defaultNS: 'common', + detection: { + order: ['localStorage', 'navigator'], + lookupLocalStorage: 'fsm_lang', + caches: ['localStorage'], + }, + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/ui/index.js b/ui/index.js index 60afb9cd..27bed460 100644 --- a/ui/index.js +++ b/ui/index.js @@ -1,10 +1,15 @@ import regeneratorRuntime from "regenerator-runtime" +import './i18n'; import Bus from "./notifications" -import React from 'react'; +import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import App from './App/App.jsx'; window.flash = (message, color="gray-light") => Bus.emit('flash', ({message, color})); const root = ReactDOM.createRoot(document.getElementById('app')); -root.render(); +root.render( + Loading...}> + + +); diff --git a/ui/locales/en/common.json b/ui/locales/en/common.json new file mode 100644 index 00000000..46e48a01 --- /dev/null +++ b/ui/locales/en/common.json @@ -0,0 +1,42 @@ +{ + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "delete": "Delete", + "loading": "Loading...", + "errorOccurred": "An error occurred", + "loginFailed": "Login failed. Please check your credentials.", + "name": "Name", + "actions": "Actions", + "username": "Username", + "password": "Password", + "email": "Email", + "role": "Role", + "required": "Required", + "signIn": "Sign In", + "logout": "Logout", + "saveSettings": "Save Settings", + "saveLastModified": "Last Modified", + "size": "Size", + "lastModifiedAt": "Last Modified At", + "settingsSaved": "Settings saved.", + "download": "Download", + "upload": "Upload", + "search": "Search", + "close": "Close", + "back": "Back", + "ok": "OK", + "yes": "Yes", + "no": "No", + "enabled": "Enabled", + "disabled": "Disabled", + "unknown": "Unknown", + "none": "None", + "edit": "Edit", + "create": "Create", + "remove": "Remove", + "add": "Add", + "update": "Update", + "refresh": "Refresh", + "status": "Status" +} diff --git a/ui/locales/en/console.json b/ui/locales/en/console.json new file mode 100644 index 00000000..c0a119ac --- /dev/null +++ b/ui/locales/en/console.json @@ -0,0 +1,8 @@ +{ + "console": "Console", + "consoleNotAvailable": "Console is not available when the server is not running.", + "inputPlaceholder": "Enter command...", + "commandSent": "Command sent.", + "rconNotAvailable": "RCON is not available.", + "send": "Send" +} diff --git a/ui/locales/en/controls.json b/ui/locales/en/controls.json new file mode 100644 index 00000000..39cd8602 --- /dev/null +++ b/ui/locales/en/controls.json @@ -0,0 +1,20 @@ +{ + "serverStatus": "Server Status", + "startServer": "Start Server", + "saveStopServer": "Save & Stop Server", + "killServer": "Kill Server", + "status": "Status", + "ip": "IP", + "port": "Port", + "factorioVersion": "Factorio Version", + "save": "Save", + "ipRequired": "IP is required and must be valid.", + "portRequired": "Port is required within range 1-65535", + "saveRequired": "Save is required and must be valid.", + "RUNNING": "Running", + "STOPPED": "Stopped", + "UNKNOWN": "Unknown", + "saveBeforeStop": "Save before stopping?", + "confirmStop": "Are you sure you want to stop the server?", + "confirmKill": "Are you sure you want to kill the server? This may cause data loss." +} diff --git a/ui/locales/en/layout.json b/ui/locales/en/layout.json new file mode 100644 index 00000000..3b744066 --- /dev/null +++ b/ui/locales/en/layout.json @@ -0,0 +1,21 @@ +{ + "appTitle": "Factorio Server Manager", + "serverStatus": "Server Status", + "serverManagement": "Server Management", + "fsmAdministration": "FSM Administration", + "linkControls": "Controls", + "linkSaves": "Saves", + "linkMods": "Mods", + "linkServerSettings": "Server Settings", + "linkGameSettings": "Game Settings", + "linkConsole": "Console", + "linkLogs": "Logs", + "linkUsers": "Users", + "linkHelp": "Help", + "languageLabel": "Language", + "helpTitle": "Help", + "helpBugsAndHelp": "Bugs and Help", + "helpDescription": "For bug reports, feature requests, or help with Factorio Server Manager, please visit the following resources:", + "helpResources": "Resources", + "helpWiki": "Wiki" +} diff --git a/ui/locales/en/logs.json b/ui/locales/en/logs.json new file mode 100644 index 00000000..3b699aeb --- /dev/null +++ b/ui/locales/en/logs.json @@ -0,0 +1,8 @@ +{ + "logs": "Logs", + "noLogsAvailable": "No logs available.", + "factorioLog": "Factorio Log", + "factorioCurrentLog": "Factorio Current Log", + "factorioPreviousLog": "Factorio Previous Log", + "fsmLog": "FSM Log" +} diff --git a/ui/locales/en/mods.json b/ui/locales/en/mods.json new file mode 100644 index 00000000..6c8e3904 --- /dev/null +++ b/ui/locales/en/mods.json @@ -0,0 +1,32 @@ +{ + "mods": "Mods", + "modPacks": "Mod Packs", + "installMod": "Install Mod", + "uploadMod": "Upload Mod", + "loadModsFromSave": "Load Mods from Save", + "changingModsDisabled": "Changing mods is disabled while the server is running. Please stop the server before making changes.", + "deleteAllMods": "Delete all Mods", + "updateAllMods": "Update all Mods", + "downloadAllMods": "Download all Mods", + "confirmDeleteMod": "Are you sure you want to delete this mod?", + "noModsInstalled": "No mods installed.", + "searchMods": "Search mods...", + "install": "Install", + "modPortal": "Mod Portal", + "loadingModList": "Loading mod list...", + "uploadModTitle": "Upload Mod", + "selectModFile": "Select a mod file", + "createModPack": "Create Mod Pack", + "modPackName": "Mod Pack Name", + "modsInThisPack": "Mods in this pack", + "factorioLogin": "Factorio.com Login", + "token": "Token", + "login": "Login", + "selectVersion": "Select Version", + "version": "Version", + "loadMods": "Load Mods", + "loadModsTitle": "Load Mods from Save", + "selectSave": "Select a save file", + "modsLoaded": "Mods loaded from save.", + "deleteExistingMods": "Delete all existing mods before loading? Existing mods will be permanently removed." +} diff --git a/ui/locales/en/saves.json b/ui/locales/en/saves.json new file mode 100644 index 00000000..b3f52b7c --- /dev/null +++ b/ui/locales/en/saves.json @@ -0,0 +1,13 @@ +{ + "createSave": "Create Save", + "uploadSave": "Upload Save", + "saves": "Saves", + "createSaveDisabled": "Creating saves is disabled while the server is running.", + "confirmDeleteSave": "Are you sure you want to delete this save?", + "downloadSave": "Download", + "noSavesFound": "No saves found.", + "savefileName": "Save File Name", + "saveFile": "Save File", + "selectFile": "Select File ...", + "saveNameRequired": "Save name is required." +} diff --git a/ui/locales/en/serverSettings.json b/ui/locales/en/serverSettings.json new file mode 100644 index 00000000..1e85bf1b --- /dev/null +++ b/ui/locales/en/serverSettings.json @@ -0,0 +1,30 @@ +{ + "serverSettings": "Server Settings", + "gameSettings": "Game Settings", + "saveSettings": "Save Settings", + "settingsSaved": "Settings saved.", + "visibility": "Visibility", + "public": "Public", + "lan": "LAN", + "hidden": "Hidden", + "requireUserVerification": "Require User Verification", + "maxPlayers": "Max Players", + "gamePassword": "Game Password", + "description": "Description", + "tags": "Tags", + "autoPause": "Auto Pause", + "onlyAdminsCanPause": "Only admins can pause", + "autosaveInterval": "Autosave Interval (minutes)", + "autosaveSlots": "Autosave Slots", + "afkAutokickInterval": "AFK Auto-kick Interval (minutes)", + "allowCommands": "Allow Commands", + "resourcePatchSize": "Resource Patch Size", + "resourceFrequency": "Resource Frequency", + "resourceRichness": "Resource Richness", + "enemyBaseSize": "Enemy Base Size", + "enemyBaseFrequency": "Enemy Base Frequency", + "startingAreaSize": "Starting Area Size", + "peacefulMode": "Peaceful Mode", + "researchQueue": "Research Queue", + "technologyPriceMultiplier": "Technology Price Multiplier" +} diff --git a/ui/locales/en/userManagement.json b/ui/locales/en/userManagement.json new file mode 100644 index 00000000..8b9b6e83 --- /dev/null +++ b/ui/locales/en/userManagement.json @@ -0,0 +1,20 @@ +{ + "listOfUsers": "List of Users", + "changePassword": "Change Password", + "createUser": "Create User", + "confirmDeleteUser": "Are you sure you want to delete this user?", + "noUsersFound": "No users found.", + "passwordConfirmation": "Password Confirmation", + "oldPassword": "Old Password", + "newPassword": "New Password", + "newPasswordConfirmation": "New Password Confirmation", + "passwordRequired": "Password is required.", + "passwordMismatch": "Passwords do not match.", + "passwordChanged": "Password changed.", + "usernameRequired": "Username is required.", + "roleRequired": "Role is required.", + "emailRequired": "Email is required.", + "userCreated": "User created.", + "userDeleted": "User deleted.", + "change": "Change" +} From af63fe9efc0244b30a24ff2837bf831842d0d722 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 03:30:20 +0000 Subject: [PATCH 04/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E5=AE=89=E8=A3=85?= =?UTF-8?q?=20i18n=20=E4=BE=9D=E8=B5=96=20+=20=E6=B7=BB=E5=8A=A0=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E7=BF=BB=E8=AF=91=E6=96=87=E4=BB=B6=EF=BC=889=20?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 172 ++++++++++++++++++++++----- package.json | 3 + ui/locales/zh-CN/common.json | 42 +++++++ ui/locales/zh-CN/console.json | 8 ++ ui/locales/zh-CN/controls.json | 20 ++++ ui/locales/zh-CN/layout.json | 21 ++++ ui/locales/zh-CN/logs.json | 8 ++ ui/locales/zh-CN/mods.json | 32 +++++ ui/locales/zh-CN/saves.json | 13 ++ ui/locales/zh-CN/serverSettings.json | 30 +++++ ui/locales/zh-CN/userManagement.json | 20 ++++ 11 files changed, 341 insertions(+), 28 deletions(-) create mode 100644 ui/locales/zh-CN/common.json create mode 100644 ui/locales/zh-CN/console.json create mode 100644 ui/locales/zh-CN/controls.json create mode 100644 ui/locales/zh-CN/layout.json create mode 100644 ui/locales/zh-CN/logs.json create mode 100644 ui/locales/zh-CN/mods.json create mode 100644 ui/locales/zh-CN/saves.json create mode 100644 ui/locales/zh-CN/serverSettings.json create mode 100644 ui/locales/zh-CN/userManagement.json diff --git a/package-lock.json b/package-lock.json index eae1b4f2..9e09ea4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.0", "fuse.js": "^7.0.0", + "i18next": "^26.3.1", + "i18next-browser-languagedetector": "^8.2.1", "react-hook-form": "^7.47.0", + "react-i18next": "^17.0.8", "regenerator-runtime": "^0.14.0", "semver": "^7.3.7", "tailwindcss": "^3.3.5" @@ -1831,23 +1834,14 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz", - "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true - }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -3923,6 +3917,52 @@ "node": ">= 0.4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.3.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", + "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -5410,6 +5450,33 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6258,11 +6325,29 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -7722,21 +7807,9 @@ "dev": true }, "@babel/runtime": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz", - "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true - } - } + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==" }, "@babel/template": { "version": "7.22.15", @@ -9245,6 +9318,28 @@ "function-bind": "^1.1.2" } }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, + "i18next": { + "version": "26.3.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", + "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "requires": {} + }, + "i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "requires": { + "@babel/runtime": "^7.23.2" + } + }, "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -10228,6 +10323,16 @@ "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", "requires": {} }, + "react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "requires": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10807,11 +10912,22 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index c0abfe46..a47ad41e 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.0", "fuse.js": "^7.0.0", + "i18next": "^26.3.1", + "i18next-browser-languagedetector": "^8.2.1", "react-hook-form": "^7.47.0", + "react-i18next": "^17.0.8", "regenerator-runtime": "^0.14.0", "semver": "^7.3.7", "tailwindcss": "^3.3.5" diff --git a/ui/locales/zh-CN/common.json b/ui/locales/zh-CN/common.json new file mode 100644 index 00000000..b4becf0b --- /dev/null +++ b/ui/locales/zh-CN/common.json @@ -0,0 +1,42 @@ +{ + "save": "保存", + "cancel": "取消", + "confirm": "确认", + "delete": "删除", + "loading": "加载中...", + "errorOccurred": "发生错误", + "loginFailed": "登录失败,请检查凭据。", + "name": "名称", + "actions": "操作", + "username": "用户名", + "password": "密码", + "email": "邮箱", + "role": "角色", + "required": "必填", + "signIn": "登录", + "logout": "退出登录", + "saveSettings": "保存设置", + "saveLastModified": "最后修改", + "size": "大小", + "lastModifiedAt": "最后修改时间", + "settingsSaved": "设置已保存。", + "download": "下载", + "upload": "上传", + "search": "搜索", + "close": "关闭", + "back": "返回", + "ok": "确定", + "yes": "是", + "no": "否", + "enabled": "已启用", + "disabled": "已禁用", + "unknown": "未知", + "none": "无", + "edit": "编辑", + "create": "创建", + "remove": "移除", + "add": "添加", + "update": "更新", + "refresh": "刷新", + "status": "状态" +} diff --git a/ui/locales/zh-CN/console.json b/ui/locales/zh-CN/console.json new file mode 100644 index 00000000..4a43e043 --- /dev/null +++ b/ui/locales/zh-CN/console.json @@ -0,0 +1,8 @@ +{ + "console": "控制台", + "consoleNotAvailable": "服务器未运行时控制台不可用。", + "inputPlaceholder": "输入指令...", + "commandSent": "指令已发送。", + "rconNotAvailable": "RCON 不可用。", + "send": "发送" +} diff --git a/ui/locales/zh-CN/controls.json b/ui/locales/zh-CN/controls.json new file mode 100644 index 00000000..d1420efe --- /dev/null +++ b/ui/locales/zh-CN/controls.json @@ -0,0 +1,20 @@ +{ + "serverStatus": "服务器状态", + "startServer": "启动服务器", + "saveStopServer": "保存并停止服务器", + "killServer": "强制停止服务器", + "status": "状态", + "ip": "IP 地址", + "port": "端口", + "factorioVersion": "Factorio 版本", + "save": "存档", + "ipRequired": "IP 地址为必填项且必须有效。", + "portRequired": "端口为必填项,范围 1-65535", + "saveRequired": "存档为必填项且必须有效。", + "RUNNING": "运行中", + "STOPPED": "已停止", + "UNKNOWN": "未知", + "saveBeforeStop": "停止前保存游戏?", + "confirmStop": "确认停止服务器?", + "confirmKill": "确认强制停止服务器?这可能导致数据丢失。" +} diff --git a/ui/locales/zh-CN/layout.json b/ui/locales/zh-CN/layout.json new file mode 100644 index 00000000..84353f39 --- /dev/null +++ b/ui/locales/zh-CN/layout.json @@ -0,0 +1,21 @@ +{ + "appTitle": "Factorio 服务器管理器", + "serverStatus": "服务器状态", + "serverManagement": "服务器管理", + "fsmAdministration": "FSM 管理", + "linkControls": "控制面板", + "linkSaves": "存档", + "linkMods": "模组", + "linkServerSettings": "服务器设置", + "linkGameSettings": "游戏设置", + "linkConsole": "控制台", + "linkLogs": "日志", + "linkUsers": "用户", + "linkHelp": "帮助", + "languageLabel": "语言", + "helpTitle": "帮助", + "helpBugsAndHelp": "Bug 与帮助", + "helpDescription": "报告 Bug、请求新功能或获取 Factorio 服务器管理器的帮助,请访问以下资源:", + "helpResources": "资源", + "helpWiki": "Wiki" +} diff --git a/ui/locales/zh-CN/logs.json b/ui/locales/zh-CN/logs.json new file mode 100644 index 00000000..9570d675 --- /dev/null +++ b/ui/locales/zh-CN/logs.json @@ -0,0 +1,8 @@ +{ + "logs": "日志", + "noLogsAvailable": "暂无日志。", + "factorioLog": "Factorio 日志", + "factorioCurrentLog": "Factorio 当前日志", + "factorioPreviousLog": "Factorio 历史日志", + "fsmLog": "FSM 日志" +} diff --git a/ui/locales/zh-CN/mods.json b/ui/locales/zh-CN/mods.json new file mode 100644 index 00000000..833e7cd3 --- /dev/null +++ b/ui/locales/zh-CN/mods.json @@ -0,0 +1,32 @@ +{ + "mods": "模组", + "modPacks": "模组合集", + "installMod": "安装模组", + "uploadMod": "上传模组", + "loadModsFromSave": "从存档加载模组", + "changingModsDisabled": "服务器运行中无法更改模组。请先停止服务器。", + "deleteAllMods": "删除所有模组", + "updateAllMods": "更新所有模组", + "downloadAllMods": "下载所有模组", + "confirmDeleteMod": "确认删除此模组?", + "noModsInstalled": "未安装任何模组。", + "searchMods": "搜索模组...", + "install": "安装", + "modPortal": "模组门户", + "loadingModList": "正在加载模组列表...", + "uploadModTitle": "上传模组", + "selectModFile": "选择一个模组文件", + "createModPack": "创建模组合集", + "modPackName": "模组合集名称", + "modsInThisPack": "此合集中的模组", + "factorioLogin": "Factorio.com 登录", + "token": "令牌", + "login": "登录", + "selectVersion": "选择版本", + "version": "版本", + "loadMods": "加载模组", + "loadModsTitle": "从存档加载模组", + "selectSave": "选择一个存档文件", + "modsLoaded": "已从存档加载模组。", + "deleteExistingMods": "加载前删除所有现有模组?现有模组将被永久移除。" +} diff --git a/ui/locales/zh-CN/saves.json b/ui/locales/zh-CN/saves.json new file mode 100644 index 00000000..ea9af1b6 --- /dev/null +++ b/ui/locales/zh-CN/saves.json @@ -0,0 +1,13 @@ +{ + "createSave": "创建存档", + "uploadSave": "上传存档", + "saves": "存档", + "createSaveDisabled": "服务器运行中无法创建存档。", + "confirmDeleteSave": "确认删除此存档?", + "downloadSave": "下载", + "noSavesFound": "未找到存档。", + "savefileName": "存档文件名", + "saveFile": "存档文件", + "selectFile": "选择文件 ...", + "saveNameRequired": "存档名称为必填项。" +} diff --git a/ui/locales/zh-CN/serverSettings.json b/ui/locales/zh-CN/serverSettings.json new file mode 100644 index 00000000..d49ca1f9 --- /dev/null +++ b/ui/locales/zh-CN/serverSettings.json @@ -0,0 +1,30 @@ +{ + "serverSettings": "服务器设置", + "gameSettings": "游戏设置", + "saveSettings": "保存设置", + "settingsSaved": "设置已保存。", + "visibility": "可见性", + "public": "公开", + "lan": "局域网", + "hidden": "隐藏", + "requireUserVerification": "需要用户验证", + "maxPlayers": "最大玩家数", + "gamePassword": "游戏密码", + "description": "描述", + "tags": "标签", + "autoPause": "自动暂停", + "onlyAdminsCanPause": "仅管理员可暂停", + "autosaveInterval": "自动保存间隔(分钟)", + "autosaveSlots": "自动保存槽位", + "afkAutokickInterval": "AFK 自动踢出间隔(分钟)", + "allowCommands": "允许指令", + "resourcePatchSize": "资源矿脉大小", + "resourceFrequency": "资源频率", + "resourceRichness": "资源富饶度", + "enemyBaseSize": "虫巢大小", + "enemyBaseFrequency": "虫巢频率", + "startingAreaSize": "起始区域大小", + "peacefulMode": "和平模式", + "researchQueue": "研究队列", + "technologyPriceMultiplier": "科技价格倍率" +} diff --git a/ui/locales/zh-CN/userManagement.json b/ui/locales/zh-CN/userManagement.json new file mode 100644 index 00000000..23a8b199 --- /dev/null +++ b/ui/locales/zh-CN/userManagement.json @@ -0,0 +1,20 @@ +{ + "listOfUsers": "用户列表", + "changePassword": "修改密码", + "createUser": "创建用户", + "confirmDeleteUser": "确认删除此用户?", + "noUsersFound": "未找到用户。", + "passwordConfirmation": "确认密码", + "oldPassword": "旧密码", + "newPassword": "新密码", + "newPasswordConfirmation": "确认新密码", + "passwordRequired": "密码为必填项。", + "passwordMismatch": "两次输入的密码不一致。", + "passwordChanged": "密码已修改。", + "usernameRequired": "用户名为必填项。", + "roleRequired": "角色为必填项。", + "emailRequired": "邮箱为必填项。", + "userCreated": "用户已创建。", + "userDeleted": "用户已删除。", + "change": "修改" +} From a2b7376fa0fb5c0902f46940698a21ea12d5b492 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 04:25:39 +0000 Subject: [PATCH 05/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20ConfirmDialog=20+?= =?UTF-8?q?=20Layout=20=E5=9B=BD=E9=99=85=E5=8C=96=EF=BC=88=E5=90=AB?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2=E5=99=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/components/ConfirmDialog.jsx | 6 ++-- ui/App/components/Layout.jsx | 48 +++++++++++++++++++---------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ui/App/components/ConfirmDialog.jsx b/ui/App/components/ConfirmDialog.jsx index 7f96c9bc..3e902068 100644 --- a/ui/App/components/ConfirmDialog.jsx +++ b/ui/App/components/ConfirmDialog.jsx @@ -1,9 +1,11 @@ import React, {useState} from 'react'; +import { useTranslation } from 'react-i18next'; import Modal from "./Modal"; import Button from "./Button"; function ConfirmDialog({title, content, isOpen, close, onSuccess}) { + const { t } = useTranslation('common'); const [isLoading, setIsLoading] = useState(false); const confirm = () => { @@ -21,8 +23,8 @@ function ConfirmDialog({title, content, isOpen, close, onSuccess}) { content={content} actions={ <> - - + + } isOpen={isOpen} diff --git a/ui/App/components/Layout.jsx b/ui/App/components/Layout.jsx index 2f47a676..87ccd14f 100644 --- a/ui/App/components/Layout.jsx +++ b/ui/App/components/Layout.jsx @@ -1,4 +1,6 @@ import React, {useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; +import i18n from '../../i18n'; import {NavLink, Outlet} from "react-router-dom"; import Button from "./Button"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; @@ -7,18 +9,19 @@ import {Flash} from "./Flash"; const Layout = ({handleLogout, serverStatus}) => { + const { t } = useTranslation(['layout', 'common', 'controls']); const [isNavCollapsed, setIsNavCollapsed] = useState(true); const Status = ({info}) => { - let text = 'Unknown'; + let text = t('UNKNOWN', { ns: 'controls' }); let color = 'gray-light'; if (info && info.running) { - text = 'Running'; + text = t('RUNNING', { ns: 'controls' }); color = 'green'; } else if (info && !info.running) { - text = 'Stopped'; + text = t('STOPPED', { ns: 'controls' }); color = 'red'; } @@ -48,7 +51,7 @@ const Layout = ({handleLogout, serverStatus}) => {
- Factorio Server Manager + {t('appTitle', { ns: 'layout' })}
-

Server Status

+

{t('serverStatus', { ns: 'layout' })}

-

Server Management

+

{t('serverManagement', { ns: 'layout' })}

- Controls - Saves - Mods - Server Settings - Game Settings - Console - Logs + {t('linkControls', { ns: 'layout' })} + {t('linkSaves', { ns: 'layout' })} + {t('linkMods', { ns: 'layout' })} + {t('linkServerSettings', { ns: 'layout' })} + {t('linkGameSettings', { ns: 'layout' })} + {t('linkConsole', { ns: 'layout' })} + {t('linkLogs', { ns: 'layout' })}
-

FSM Administration

+

{t('fsmAdministration', { ns: 'layout' })}

- Users - Help + {t('linkUsers', { ns: 'layout' })} + {t('linkHelp', { ns: 'layout' })} +
+
+ +
- +
From b0ee691a90e586f27c517eea4310ed658cbd85f0 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 04:29:30 +0000 Subject: [PATCH 06/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20FSM=20=E5=89=8D=E7=AB=AF=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=88zh-CN=20=E4=B8=AD=E6=96=87=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/build-release.sh | 0 docker/build.sh | 0 docker/entrypoint.sh | 0 ui/App/views/Console.jsx | 7 +++- ui/App/views/Controls.jsx | 42 ++++++++++--------- ui/App/views/GameSettings.jsx | 5 ++- ui/App/views/Help.jsx | 10 +++-- ui/App/views/Login.jsx | 18 ++++---- ui/App/views/Logs.jsx | 5 ++- ui/App/views/Mods/Mods.jsx | 25 ++++++----- .../AddMod/components/AddModForm.jsx | 14 ++++--- .../AddMod/components/SelectVersionForm.jsx | 13 +++--- .../views/Mods/components/CreateModPack.jsx | 13 +++--- ui/App/views/Mods/components/LoadMods.jsx | 13 +++--- ui/App/views/Mods/components/ModList.jsx | 14 ++++--- ui/App/views/Mods/components/ModPack.jsx | 7 +++- ui/App/views/Mods/components/UploadMod.jsx | 8 ++-- ui/App/views/Saves/Saves.jsx | 20 +++++---- .../views/Saves/components/CreateSaveForm.jsx | 8 ++-- .../views/Saves/components/UploadSaveForm.jsx | 10 +++-- ui/App/views/ServerSettings.jsx | 8 ++-- ui/App/views/UserManagement/UserManagment.jsx | 17 ++++---- .../components/ChangePasswordForm.jsx | 24 ++++++----- .../components/CreateUserForm.jsx | 34 ++++++++------- ui/locales/en/mods.json | 5 ++- ui/locales/zh-CN/mods.json | 5 ++- 26 files changed, 191 insertions(+), 134 deletions(-) mode change 100755 => 100644 docker/build-release.sh mode change 100755 => 100644 docker/build.sh mode change 100755 => 100644 docker/entrypoint.sh diff --git a/docker/build-release.sh b/docker/build-release.sh old mode 100755 new mode 100644 diff --git a/docker/build.sh b/docker/build.sh old mode 100755 new mode 100644 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh old mode 100755 new mode 100644 diff --git a/ui/App/views/Console.jsx b/ui/App/views/Console.jsx index 16d633f4..173fa82b 100644 --- a/ui/App/views/Console.jsx +++ b/ui/App/views/Console.jsx @@ -1,9 +1,12 @@ import Panel from "../components/Panel"; import React, {useEffect, useRef, useState} from "react"; +import { useTranslation } from 'react-i18next'; import socket from "../../api/socket"; const Console = ({serverStatus}) => { + const { t } = useTranslation('console'); + const [logs, setLogs] = useState([]); const consoleInput = useRef(null); @@ -25,7 +28,7 @@ const Console = ({serverStatus}) => { return ( @@ -44,7 +47,7 @@ const Console = ({serverStatus}) => { /> :

- The console is not available, because Factorio is not running. + {t('consoleNotAvailable')}

} /> diff --git a/ui/App/views/Controls.jsx b/ui/App/views/Controls.jsx index 62b31d9e..44a46af2 100644 --- a/ui/App/views/Controls.jsx +++ b/ui/App/views/Controls.jsx @@ -1,4 +1,5 @@ import React, {useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; import Panel from "../components/Panel"; import Button from "../components/Button"; import server from "../../api/resources/server"; @@ -10,7 +11,8 @@ import Error from "../components/Error"; const Controls = ({serverStatus}) => { - const factorioVersion = serverStatus.fac_version ? serverStatus.fac_version : 'Unknown'; + const { t } = useTranslation('controls'); + const factorioVersion = serverStatus.fac_version ? serverStatus.fac_version : t('UNKNOWN'); const [saves, setSaves] = useState([]); const [isDisabled, setIsDisabled] = useState(true); const [isStopping, setIsStopping] = useState(false); @@ -48,48 +50,48 @@ const Controls = ({serverStatus}) => { return (
{ serverStatus.running ? <>
-
Status
-
{serverStatus.running ? 'Running' : 'Stopped'}
+
{t('status')}
+
{serverStatus.running ? t('RUNNING') : t('STOPPED')}
-
IP
+
{t('ip')}
{serverStatus.bindip}
-
Port
+
{t('port')}
{serverStatus.port}
-
Factorio Version
+
{t('factorioVersion')}
{factorioVersion}
-
Save
+
{t('save')}
{serverStatus.savefile}
: <>
-
Status
-
{serverStatus.running ? 'Running' : 'Stopped'}
+
{t('status')}
+
{serverStatus.running ? t('RUNNING') : t('STOPPED')}
-
IP
+
{t('ip')}
- +
-
Port
+
{t('port')}
{ disabled={isDisabled} register={register('port',{required: true, min: 1, max: 65535})} /> - +
-
Factorio Version
+
{t('factorioVersion')}
{factorioVersion}
-
Save
+
{t('save')}
- +
-
- +
} diff --git a/ui/App/views/Logs.jsx b/ui/App/views/Logs.jsx index e416402a..5ecd0022 100644 --- a/ui/App/views/Logs.jsx +++ b/ui/App/views/Logs.jsx @@ -1,9 +1,12 @@ import React, {useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; import log from "../../api/resources/log"; import Panel from "../components/Panel"; const Logs = () => { + const { t } = useTranslation('logs'); + const [logs, setLogs] = useState([]) useEffect(() => { @@ -15,7 +18,7 @@ const Logs = () => { return ( {logs.map((log,index) => (
  • {log}
  • ))} diff --git a/ui/App/views/Mods/Mods.jsx b/ui/App/views/Mods/Mods.jsx index 6376ae0a..23158e6e 100644 --- a/ui/App/views/Mods/Mods.jsx +++ b/ui/App/views/Mods/Mods.jsx @@ -1,5 +1,6 @@ import Panel from "../../components/Panel"; import React, {useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; import modsResource from "../../../api/resources/mods"; import Button from "../../components/Button"; import server from "../../../api/resources/server"; @@ -15,6 +16,8 @@ import ModList from "./components/ModList"; const Mods = ({serverStatus}) => { + const { t } = useTranslation('mods'); + const [installedMods, setInstalledMods] = useState([]); const [modPacks, setModPacks] = useState([]) const [factorioVersion, setFactorioVersion] = useState(null); @@ -110,26 +113,26 @@ const Mods = ({serverStatus}) => { {disabled ? - Changing mods is disabled while the server is running! -
    +
    + {t('changingModsDisabled')} +
    } /> : - + - + - + } { { !disabled && && + onClick={deleteAllMods}>{t('deleteAllMods')} && + onClick={updateAllMods}>{t('updateAllMods')} } Download all Mods + href={modsResource.downloadAllURL}>{t('downloadAllMods')} } /> { - return Mod - Portal + const { t } = useTranslation('mods'); + return {t('modPortal')} } const AddModForm = ({setIsFactorioAuthenticated, fuse, refetchInstalledMods}) => { + const { t } = useTranslation('mods'); const {register, watch, setValue, handleSubmit} = useForm(); const [suggestedMods, setSuggestedMods] = useState([]); const [selectedMod, setSelectedMod] = useState(null); @@ -101,11 +103,11 @@ const AddModForm = ({setIsFactorioAuthenticated, fuse, refetchInstalledMods}) =>
    setIsModalOpen(false)}/>
    -
    - - + + ) diff --git a/ui/App/views/Mods/components/AddMod/components/SelectVersionForm.jsx b/ui/App/views/Mods/components/AddMod/components/SelectVersionForm.jsx index dd2815f9..2569b7a1 100644 --- a/ui/App/views/Mods/components/AddMod/components/SelectVersionForm.jsx +++ b/ui/App/views/Mods/components/AddMod/components/SelectVersionForm.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from 'react-i18next'; import Modal from "../../../../../components/Modal"; import Button from "../../../../../components/Button"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; @@ -8,6 +9,8 @@ import {faTimes} from "@fortawesome/free-solid-svg-icons/faTimes"; const SelectVersionForm = ({releases, isOpen, close, install}) => { + const { t } = useTranslation(['mods', 'common']); + const download = release => { install(release) close() @@ -16,15 +19,15 @@ const SelectVersionForm = ({releases, isOpen, close, install}) => { return ( - - - + + + @@ -45,7 +48,7 @@ const SelectVersionForm = ({releases, isOpen, close, install}) => { } actions={ - + } /> ) diff --git a/ui/App/views/Mods/components/CreateModPack.jsx b/ui/App/views/Mods/components/CreateModPack.jsx index 94262200..5323ba49 100644 --- a/ui/App/views/Mods/components/CreateModPack.jsx +++ b/ui/App/views/Mods/components/CreateModPack.jsx @@ -1,4 +1,5 @@ import React, {useState} from "react"; +import { useTranslation } from 'react-i18next'; import Button from "../../../components/Button"; import Modal from "../../../components/Modal"; import Label from "../../../components/Label"; @@ -8,6 +9,8 @@ import modsResource from "../../../../api/resources/mods"; const CreateModPack = ({onSuccess}) => { + const { t } = useTranslation(['mods', 'common']); + const [isCreating, setIsCreating] = useState(false); const [isOpen, setIsOpen] = useState(false); @@ -26,18 +29,18 @@ const CreateModPack = ({onSuccess}) => { } return <> - - setIsOpen(true)}>{t('createModPack')} +
    -
    - + } actions={ - + } /> diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index 331c3527..06609467 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -1,4 +1,5 @@ import React, {useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; import savesResource from "../../../../api/resources/saves"; import Select from "../../../components/Select"; import Label from "../../../components/Label"; @@ -11,6 +12,8 @@ import ConfirmDialog from "../../../components/ConfirmDialog"; const LoadMods = ({refreshMods}) => { + const { t } = useTranslation(['mods', 'common']); + const [saves, setSaves] = useState([]); const {register, reset, handleSubmit} = useForm(); const [isLoading, setIsLoading] = useState(false); @@ -46,7 +49,7 @@ const LoadMods = ({refreshMods}) => { await modResource.portal.installMultiple(mods) .then(() => { refreshMods(); - window.flash(`Mods are loaded from save file ${data.save}.`, "green"); + window.flash(t('modsLoaded').replace('{save}', data.save), "green"); }).finally(() => { setIsLoading(false); setLoadModsData(undefined); @@ -55,7 +58,7 @@ const LoadMods = ({refreshMods}) => { return isFactorioAuthenticated ?
    -
    VersionCompatibilityActions{t('version', { ns: 'mods' })}{t('compatibility', { ns: 'mods' })}{t('actions', { ns: 'common' })}
    - - - - - + + + + + diff --git a/ui/App/views/Mods/components/ModPack.jsx b/ui/App/views/Mods/components/ModPack.jsx index 1b8bca0a..254f8268 100644 --- a/ui/App/views/Mods/components/ModPack.jsx +++ b/ui/App/views/Mods/components/ModPack.jsx @@ -1,4 +1,5 @@ import React, {useState} from "react"; +import { useTranslation } from 'react-i18next'; import {faSpinner, faTrashAlt, faUpload} from "@fortawesome/free-solid-svg-icons"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import modsResource from "../../../../api/resources/mods"; @@ -7,6 +8,8 @@ import ConfirmDialog from "../../../components/ConfirmDialog"; const ModPack = ({modPack, reloadModPacks, factorioVersion, reloadMods, disabled = false}) => { + const { t } = useTranslation('mods'); + const [isLoading, setIsLoading] = useState(false); const [isLoadModPackDialogOpen, setIsLoadModPackDialogOpen] = useState(false); @@ -63,8 +66,8 @@ const ModPack = ({modPack, reloadModPacks, factorioVersion, reloadMods, disabled icon={isLoading ? faSpinner : faUpload} /> setIsLoadModPackDialogOpen(false)} onSuccess={() => loadModPack(modPack.name)} diff --git a/ui/App/views/Mods/components/UploadMod.jsx b/ui/App/views/Mods/components/UploadMod.jsx index f1083508..c31661db 100644 --- a/ui/App/views/Mods/components/UploadMod.jsx +++ b/ui/App/views/Mods/components/UploadMod.jsx @@ -1,4 +1,5 @@ import React, {useState} from "react"; +import { useTranslation } from 'react-i18next'; import Button from "../../../components/Button"; import Label from "../../../components/Label"; import {useForm} from "react-hook-form"; @@ -6,7 +7,8 @@ import modsResource from "../../../../api/resources/mods"; const UploadMod = ({refetchInstalledMods}) => { - const defaultFileName = 'Select File ...' + const { t } = useTranslation('mods'); + const defaultFileName = t('selectModFile') const [fileName, setFileName] = useState(defaultFileName); const {register, handleSubmit} = useForm(); const [isUploading, setIsUploading] = useState(false); @@ -24,7 +26,7 @@ const UploadMod = ({refetchInstalledMods}) => { return ( -
    NameEnabledCompatibilityMod VersionFactorio Version{t('name', { ns: 'common' })}{t('enabled', { ns: 'common' })}{t('compatibility')}{t('modVersion')}{t('factorioVersion')}
    - - - - + + + + diff --git a/ui/App/views/Saves/components/CreateSaveForm.jsx b/ui/App/views/Saves/components/CreateSaveForm.jsx index 10b75fb9..e550fdc3 100644 --- a/ui/App/views/Saves/components/CreateSaveForm.jsx +++ b/ui/App/views/Saves/components/CreateSaveForm.jsx @@ -1,4 +1,5 @@ import {useForm} from "react-hook-form"; +import { useTranslation } from 'react-i18next'; import Button from "../../../components/Button"; import React, {useState} from "react"; import saves from "../../../../api/resources/saves"; @@ -7,6 +8,7 @@ import Input from "../../../components/Input"; import Error from "../../../components/Error"; const CreateSaveForm = ({onSuccess}) => { + const { t } = useTranslation('saves'); const {register, handleSubmit, formState: {errors}} = useForm(); const [isLoading, setIsLoading] = useState(false); @@ -23,11 +25,11 @@ const CreateSaveForm = ({onSuccess}) => { return (
    -
    - + ) } diff --git a/ui/App/views/Saves/components/UploadSaveForm.jsx b/ui/App/views/Saves/components/UploadSaveForm.jsx index 1db27160..a2fd8f3e 100644 --- a/ui/App/views/Saves/components/UploadSaveForm.jsx +++ b/ui/App/views/Saves/components/UploadSaveForm.jsx @@ -1,13 +1,15 @@ import Button from "../../../components/Button"; import React, {useState} from "react"; +import { useTranslation } from 'react-i18next'; import {useForm} from "react-hook-form"; import saves from "../../../../api/resources/saves"; import Error from "../../../components/Error"; const UploadSaveForm = ({onSuccess}) => { + const { t } = useTranslation('saves'); const {register, handleSubmit, formState: {errors}} = useForm(); - const [fileName, setFileName] = useState('Select File ...'); + const [fileName, setFileName] = useState(t('selectFile')); const onSubmit = (data, e) => { saves.upload(data.savefile[0]).then(_ => { @@ -20,7 +22,7 @@ const UploadSaveForm = ({onSuccess}) => {
    { type="file"/>
    {fileName}
    - +
    - + ) } diff --git a/ui/App/views/ServerSettings.jsx b/ui/App/views/ServerSettings.jsx index 754feef8..a4aad960 100644 --- a/ui/App/views/ServerSettings.jsx +++ b/ui/App/views/ServerSettings.jsx @@ -1,5 +1,6 @@ import Panel from "../components/Panel"; import React, {useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; import settingsResource from "../../api/resources/settings"; import Input from "../components/Input"; import Label from "../components/Label"; @@ -10,6 +11,7 @@ import {useForm} from "react-hook-form"; const ServerSettings = () => { + const { t } = useTranslation('serverSettings'); const [settings, setSettings] = useState(); const [numberInputs, setNumberInputs] = useState([]); @@ -36,7 +38,7 @@ const ServerSettings = () => { settingsResource.server.update(data) .then(() => { fetchSettings() - .then(() => window.flash("Settings saved.", "green")) + .then(() => window.flash(t('settingsSaved'), "green")) }); } @@ -124,7 +126,7 @@ const ServerSettings = () => { return ( {settings && Object.keys(settings).map(key => { @@ -150,7 +152,7 @@ const ServerSettings = () => { } actions={ - + } /> diff --git a/ui/App/views/UserManagement/UserManagment.jsx b/ui/App/views/UserManagement/UserManagment.jsx index 34829e52..5f132abb 100644 --- a/ui/App/views/UserManagement/UserManagment.jsx +++ b/ui/App/views/UserManagement/UserManagment.jsx @@ -1,5 +1,6 @@ import Panel from "../../components/Panel"; import React, {useCallback, useEffect, useState} from "react"; +import { useTranslation } from 'react-i18next'; import user from "../../../api/resources/user"; import CreateUserForm from "./components/CreateUserForm"; import ChangePasswordForm from "./components/ChangePasswordForm" @@ -8,6 +9,8 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; const UserManagement = () => { + const { t } = useTranslation('userManagement'); + const [users, setUsers] = useState([]); const updateList = useCallback(async () => { @@ -29,15 +32,15 @@ const UserManagement = () => { return ( <> - - - - + + + + @@ -57,12 +60,12 @@ const UserManagement = () => { className="mb-4" /> } className="mb-4" /> } className="mb-4" /> diff --git a/ui/App/views/UserManagement/components/ChangePasswordForm.jsx b/ui/App/views/UserManagement/components/ChangePasswordForm.jsx index ca00c4d5..e7306fe1 100644 --- a/ui/App/views/UserManagement/components/ChangePasswordForm.jsx +++ b/ui/App/views/UserManagement/components/ChangePasswordForm.jsx @@ -1,5 +1,6 @@ import {useForm} from "react-hook-form"; import React from "react"; +import { useTranslation } from 'react-i18next'; import user from "../../../../api/resources/user"; import Button from "../../../components/Button"; import Label from "../../../components/Label"; @@ -7,6 +8,7 @@ import Input from "../../../components/Input"; import Error from "../../../components/Error"; const ChangePasswordForm = () => { + const { t } = useTranslation('userManagement'); const {register, handleSubmit, reset, formState: {errors}, watch} = useForm(); const new_password = watch("new_password"); @@ -15,7 +17,7 @@ const ChangePasswordForm = () => { const res = await user.changePassword(data); if (res) { // Update successful - window.flash("Password changed", "green") + window.flash(t('passwordChanged'), "green") reset(); } } @@ -23,30 +25,30 @@ const ChangePasswordForm = () => { return (
    -
    -
    -
    - + ) } diff --git a/ui/App/views/UserManagement/components/CreateUserForm.jsx b/ui/App/views/UserManagement/components/CreateUserForm.jsx index ce3fb369..9e9f27d6 100644 --- a/ui/App/views/UserManagement/components/CreateUserForm.jsx +++ b/ui/App/views/UserManagement/components/CreateUserForm.jsx @@ -1,5 +1,6 @@ import {useForm} from "react-hook-form"; import React from "react"; +import { useTranslation } from 'react-i18next'; import user from "../../../../api/resources/user"; import Button from "../../../components/Button"; import Label from "../../../components/Label"; @@ -7,6 +8,7 @@ import Input from "../../../components/Input"; import Error from "../../../components/Error"; const CreateUserForm = ({updateUserList}) => { + const { t } = useTranslation('userManagement'); const roleValue = "admin"; const { @@ -31,52 +33,52 @@ const CreateUserForm = ({updateUserList}) => { return (
    -
    -
    -
    -
    -
    - + ) } diff --git a/ui/locales/en/mods.json b/ui/locales/en/mods.json index 6c8e3904..88ac9683 100644 --- a/ui/locales/en/mods.json +++ b/ui/locales/en/mods.json @@ -28,5 +28,8 @@ "loadModsTitle": "Load Mods from Save", "selectSave": "Select a save file", "modsLoaded": "Mods loaded from save.", - "deleteExistingMods": "Delete all existing mods before loading? Existing mods will be permanently removed." + "deleteExistingMods": "Delete all existing mods before loading? Existing mods will be permanently removed.", + "compatibility": "Compatibility", + "modVersion": "Mod Version", + "factorioVersion": "Factorio Version" } diff --git a/ui/locales/zh-CN/mods.json b/ui/locales/zh-CN/mods.json index 833e7cd3..30aaef2c 100644 --- a/ui/locales/zh-CN/mods.json +++ b/ui/locales/zh-CN/mods.json @@ -28,5 +28,8 @@ "loadModsTitle": "从存档加载模组", "selectSave": "选择一个存档文件", "modsLoaded": "已从存档加载模组。", - "deleteExistingMods": "加载前删除所有现有模组?现有模组将被永久移除。" + "deleteExistingMods": "加载前删除所有现有模组?现有模组将被永久移除。", + "compatibility": "兼容性", + "modVersion": "模组版本", + "factorioVersion": "Factorio 版本" } From 30f789045c64f893bc0e3291812192d540fc95b6 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 04:30:44 +0000 Subject: [PATCH 07/38] =?UTF-8?q?=E6=96=87=E6=A1=A3:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=96=B0=E8=AF=AD=E8=A8=80=E9=80=82=E9=85=8D=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/add-language-guide.md | 143 +++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/add-language-guide.md diff --git a/docs/add-language-guide.md b/docs/add-language-guide.md new file mode 100644 index 00000000..05a54021 --- /dev/null +++ b/docs/add-language-guide.md @@ -0,0 +1,143 @@ +# 添加新语言指南 + +FSM 前端基于 `react-i18next`,添加新语言只需创建翻译文件并注册即可。 + +## 快速上手(以日语 ja 为例) + +### 第 1 步:创建翻译文件 + +```bash +mkdir -p ui/locales/ja +``` + +复制英文文件作为模板: + +```bash +cp ui/locales/en/*.json ui/locales/ja/ +``` + +### 第 2 步:翻译 + +逐个编辑 `ui/locales/ja/` 下的 9 个 JSON 文件,**只改 value,不改 key**。 + +| 文件 | 示例 key | 英文 | 日文翻译 | +|---|---|---|---| +| `common.json` | `save` | Save | 保存 | +| `common.json` | `cancel` | Cancel | キャンセル | +| `common.json` | `confirm` | Confirm | 確認 | +| `controls.json` | `startServer` | Start Server | サーバー起動 | +| `controls.json` | `RUNNING` | Running | 実行中 | +| `layout.json` | `appTitle` | Factorio Server Manager | Factorio サーバーマネージャー | + +### 第 3 步:注册语言 + +编辑 `ui/i18n.js`,在两个地方添加 ja: + +**位置 1 — import 翻译文件:** + +```js +// 在 zh-CN imports 后面添加 +import jaCommon from './locales/ja/common.json'; +import jaLayout from './locales/ja/layout.json'; +import jaControls from './locales/ja/controls.json'; +import jaMods from './locales/ja/mods.json'; +import jaSaves from './locales/ja/saves.json'; +import jaServerSettings from './locales/ja/serverSettings.json'; +import jaLogs from './locales/ja/logs.json'; +import jaConsole from './locales/ja/console.json'; +import jaUserManagement from './locales/ja/userManagement.json'; +``` + +**位置 2 — 添加到 resources 对象:** + +```js +const resources = { + en: { /* 已有 */ }, + 'zh-CN': { /* 已有 */ }, + ja: { + common: jaCommon, + layout: jaLayout, + controls: jaControls, + mods: jaMods, + saves: jaSaves, + serverSettings: jaServerSettings, + logs: jaLogs, + console: jaConsole, + userManagement: jaUserManagement, + }, +}; +``` + +### 第 4 步:添加到语言切换器 + +编辑 `ui/App/components/Layout.jsx`,在 `
    + { !disabled &&
    NameLast Modified AtSizeActions{t('name', { ns: 'common' })}{t('lastModifiedAt', { ns: 'common' })}{t('size', { ns: 'common' })}{t('actions', { ns: 'common' })}
    NameRoleEmailActions{t('name', { ns: 'common' })}{t('role', { ns: 'common' })}{t('email', { ns: 'common' })}{t('actions', { ns: 'common' })}
    {mod.factorio_version}{mod.dep_op ? mod.dep_op + ' ' + mod.factorio_version : mod.factorio_version} From 1298a6a2141aacad562b0dfe3f08beafc6e60efa Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:10:51 +0000 Subject: [PATCH 27/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E6=9C=AA=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=20mod=20=E7=82=B9=E5=87=BB=E5=90=AF=E7=94=A8=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=B3=A8=E5=86=8C=E5=88=B0=20mod-list.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/factorio/mod_modSimple.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/factorio/mod_modSimple.go b/src/factorio/mod_modSimple.go index 52d39bd6..4cbb0359 100644 --- a/src/factorio/mod_modSimple.go +++ b/src/factorio/mod_modSimple.go @@ -154,7 +154,11 @@ func (modSimpleList *ModSimpleList) ToggleMod(modName string) (error, bool) { } if !found { - return errors.New("mod is not installed"), newEnabled + err = modSimpleList.createMod(modName) + if err != nil { + return errors.New("mod is not installed"), newEnabled + } + newEnabled = true } err = modSimpleList.saveModInfoJson() From da412b9f707fb882cde4ecb97ba8ba841cd19d6a Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:13:28 +0000 Subject: [PATCH 28/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=20mod=20=E8=BF=94=E5=9B=9E=E6=98=8E=E7=A1=AE=20JSON=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=80=8C=E9=9D=9E=E5=8E=9F=E5=A7=8B=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mods_handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/mods_handler.go b/src/api/mods_handler.go index 49fccbea..fe9527b5 100644 --- a/src/api/mods_handler.go +++ b/src/api/mods_handler.go @@ -127,8 +127,7 @@ func ModDeleteHandler(w http.ResponseWriter, r *http.Request) { log.Println(resp) return } - - resp = data.Name + resp = map[string]string{"status": "ok", "name": data.Name} } func ModDeleteAllHandler(w http.ResponseWriter, r *http.Request) { From db6922ba1d01132146346c64b3bccae2c057fc23 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:16:08 +0000 Subject: [PATCH 29/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E4=BB=8E=E5=AD=98?= =?UTF-8?q?=E6=A1=A3=E5=8A=A0=E8=BD=BD=E6=A8=A1=E7=BB=84=E6=94=B9=E7=94=A8?= =?UTF-8?q?=20try/finally=20=E4=BF=9D=E8=AF=81=E7=8A=B6=E6=80=81=E6=B8=85?= =?UTF-8?q?=E7=90=86=E2=80=94=E2=80=94=E4=B8=8D=E5=86=8D=E6=97=A0=E9=99=90?= =?UTF-8?q?=E8=BD=AC=E5=9C=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/views/Mods/components/LoadMods.jsx | 29 +++++++++++++---------- ui/locales/en/mods.json | 1 + ui/locales/zh-CN/mods.json | 1 + 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index 06609467..c33616e6 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -40,20 +40,25 @@ const LoadMods = ({refreshMods}) => { } const loadMods = async data => { - await modResource.deleteAll(); - const {mods} = await savesResource.mods(data.save).catch(() => { + try { + await modResource.deleteAll(); + const result = await savesResource.mods(data.save); + const mods = result?.mods || []; + + if (mods.length === 0) { + window.flash(t('noModsFound', { ns: 'mods' }), "green"); + return; + } + + await modResource.portal.installMultiple(mods); + refreshMods(); + window.flash(t('modsLoaded').replace('{save}', data.save), "green"); + } catch (e) { + window.flash(t('errorOccurred', { ns: 'common' }), "red"); + } finally { setIsLoading(false); setLoadModsData(undefined); - }); - - await modResource.portal.installMultiple(mods) - .then(() => { - refreshMods(); - window.flash(t('modsLoaded').replace('{save}', data.save), "green"); - }).finally(() => { - setIsLoading(false); - setLoadModsData(undefined); - }); + } } return isFactorioAuthenticated diff --git a/ui/locales/en/mods.json b/ui/locales/en/mods.json index 88ac9683..c5fd1c6a 100644 --- a/ui/locales/en/mods.json +++ b/ui/locales/en/mods.json @@ -29,6 +29,7 @@ "selectSave": "Select a save file", "modsLoaded": "Mods loaded from save.", "deleteExistingMods": "Delete all existing mods before loading? Existing mods will be permanently removed.", + "noModsFound": "No mods found in this save.", "compatibility": "Compatibility", "modVersion": "Mod Version", "factorioVersion": "Factorio Version" diff --git a/ui/locales/zh-CN/mods.json b/ui/locales/zh-CN/mods.json index 30aaef2c..35e9fb22 100644 --- a/ui/locales/zh-CN/mods.json +++ b/ui/locales/zh-CN/mods.json @@ -29,6 +29,7 @@ "selectSave": "选择一个存档文件", "modsLoaded": "已从存档加载模组。", "deleteExistingMods": "加载前删除所有现有模组?现有模组将被永久移除。", + "noModsFound": "此存档中没有模组。", "compatibility": "兼容性", "modVersion": "模组版本", "factorioVersion": "Factorio 版本" From 42a39e5acd957bc8e03e4e17fb81534adc7c8fa1 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:25:12 +0000 Subject: [PATCH 30/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E4=BB=8E=E5=AD=98?= =?UTF-8?q?=E6=A1=A3=E5=8A=A0=E8=BD=BD=E6=A8=A1=E7=BB=84=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20WebSocket=20=E5=AE=9E=E6=97=B6=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mod_portal_handler.go | 26 +++++++++------- ui/App/views/Mods/components/LoadMods.jsx | 36 ++++++++++++++++++++++- ui/api/socket.js | 16 ++++++++++ ui/locales/en/mods.json | 1 + ui/locales/zh-CN/mods.json | 1 + 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/api/mod_portal_handler.go b/src/api/mod_portal_handler.go index ba4cb813..444fee83 100644 --- a/src/api/mod_portal_handler.go +++ b/src/api/mod_portal_handler.go @@ -5,6 +5,7 @@ import ( "log" "net/http" + "github.com/OpenFactorioServerManager/factorio-server-manager/api/websocket" "github.com/OpenFactorioServerManager/factorio-server-manager/factorio" "github.com/gorilla/mux" ) @@ -185,40 +186,43 @@ func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) { return } + wsRoom := websocket.WebsocketHub.GetRoom("mod_install") + total := len(data) + current := 0 + for _, datum := range data { // skip base mod because it is already included in factorio if datum.Name == "base" { + current++ continue } details, err, statusCode := factorio.ModPortalModDetails(datum.Name) if err != nil || statusCode != http.StatusOK { - resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err) - log.Println(resp) - w.WriteHeader(http.StatusInternalServerError) - return + log.Printf("Error in getting mod details from mod portal: %s", err) + current++ + continue } - //find correct mod-version var found = false for _, release := range details.Releases { if release.Version.Equals(datum.Version) { found = true - err := modList.DownloadMod(release.DownloadURL, release.FileName, details.Name) if err != nil { - resp = fmt.Sprintf("Error downloading mod {%s}, error: %s", details.Name, err) - log.Println(resp) - w.WriteHeader(http.StatusInternalServerError) - return + log.Printf("Error downloading mod {%s}, error: %s", details.Name, err) + break } break } } if !found { log.Printf("Error downloading mod {%s}, error: %s", details.Name, "version not found") - w.WriteHeader(http.StatusInternalServerError) } + current++ + wsRoom.Send(fmt.Sprintf(\`{"type":"progress","current":%d,"total":%d,"name":"%s"}\`, current, total, datum.Name)) } + wsRoom.Send(fmt.Sprintf(\`{"type":"complete","total":%d}\`, total)) + resp = modList.ListInstalledMods() } diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index c33616e6..368bcf28 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -9,17 +9,19 @@ import modsResource from "../../../../api/resources/mods"; import modResource from "../../../../api/resources/mods"; import FactorioLogin from "./AddMod/components/FactorioLogin"; import ConfirmDialog from "../../../components/ConfirmDialog"; +import socket from "../../../../api/socket"; const LoadMods = ({refreshMods}) => { const { t } = useTranslation(['mods', 'common']); - const [saves, setSaves] = useState([]); const {register, reset, handleSubmit} = useForm(); const [isLoading, setIsLoading] = useState(false); const [isDisabled, setIsDisabled] = useState(true); const [isFactorioAuthenticated, setIsFactorioAuthenticated] = useState(false); const [loadModsData, setLoadModsData] = useState(undefined); + const [installProgress, setInstallProgress] = useState({current: 0, total: 0}); + const [showProgress, setShowProgress] = useState(false); useEffect(() => { (async () => { @@ -32,6 +34,24 @@ const LoadMods = ({refreshMods}) => { } reset(); })(); + + const handleProgress = (msg) => { + try { + const data = JSON.parse(typeof msg === 'string' ? msg : JSON.stringify(msg)); + if (data.type === 'progress') { + setInstallProgress({current: data.current, total: data.total}); + setShowProgress(true); + } else if (data.type === 'complete') { + setShowProgress(false); + } + } catch (e) {} + }; + socket.on('mod_install', handleProgress); + socket.emit('mod install subscribe'); + + return () => { + socket.off('mod_install', handleProgress); + }; }, []); const loadModsRequested = data => { @@ -74,6 +94,20 @@ const LoadMods = ({refreshMods}) => { }))} /> + {showProgress && ( +
    +
    + {t('installingMods', { ns: 'mods' })} + {installProgress.current}/{installProgress.total} +
    +
    +
    0 ? (installProgress.current / installProgress.total * 100) : 0}%`}} + /> +
    +
    + )} { diff --git a/ui/locales/en/mods.json b/ui/locales/en/mods.json index c5fd1c6a..ab6c3761 100644 --- a/ui/locales/en/mods.json +++ b/ui/locales/en/mods.json @@ -30,6 +30,7 @@ "modsLoaded": "Mods loaded from save.", "deleteExistingMods": "Delete all existing mods before loading? Existing mods will be permanently removed.", "noModsFound": "No mods found in this save.", + "installingMods": "Installing mods", "compatibility": "Compatibility", "modVersion": "Mod Version", "factorioVersion": "Factorio Version" diff --git a/ui/locales/zh-CN/mods.json b/ui/locales/zh-CN/mods.json index 35e9fb22..e789f612 100644 --- a/ui/locales/zh-CN/mods.json +++ b/ui/locales/zh-CN/mods.json @@ -30,6 +30,7 @@ "modsLoaded": "已从存档加载模组。", "deleteExistingMods": "加载前删除所有现有模组?现有模组将被永久移除。", "noModsFound": "此存档中没有模组。", + "installingMods": "安装模组中", "compatibility": "兼容性", "modVersion": "模组版本", "factorioVersion": "Factorio 版本" From e43d47c06cbaaa583addf19a33c73e13f74ea4b2 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:25:34 +0000 Subject: [PATCH 31/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20WebSocket=20=E8=BF=9B=E5=BA=A6=E6=B6=88=E6=81=AF=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mod_portal_handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/mod_portal_handler.go b/src/api/mod_portal_handler.go index 444fee83..f56d7430 100644 --- a/src/api/mod_portal_handler.go +++ b/src/api/mod_portal_handler.go @@ -219,10 +219,10 @@ func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Error downloading mod {%s}, error: %s", details.Name, "version not found") } current++ - wsRoom.Send(fmt.Sprintf(\`{"type":"progress","current":%d,"total":%d,"name":"%s"}\`, current, total, datum.Name)) + wsRoom.Send(fmt.Sprintf("{\"type\":\"progress\",\"current\":%d,\"total\":%d,\"name\":\"%s\"}", current, total, datum.Name)) } - wsRoom.Send(fmt.Sprintf(\`{"type":"complete","total":%d}\`, total)) + wsRoom.Send(fmt.Sprintf("{\"type\":\"complete\",\"total\":%d}", total)) resp = modList.ListInstalledMods() } From a9f4105485219ee3c8f661756af6746bbe8f0ded Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:28:48 +0000 Subject: [PATCH 32/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=A8=A1=E7=BB=84=E5=90=8E=E7=AB=8B=E5=8D=B3?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=BC=B9=E7=AA=97=E2=80=94=E2=80=94=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E4=B8=8D=E8=A2=AB=E9=81=AE=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/views/Mods/components/LoadMods.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index 368bcf28..254b5bd9 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -116,7 +116,11 @@ const LoadMods = ({refreshMods}) => { setIsLoading(false); setLoadModsData(undefined); }} - onSuccess={() => loadMods(loadModsData)} + onSuccess={() => { + const data = loadModsData; + setLoadModsData(undefined); + setTimeout(() => loadMods(data), 0); + }} /> : From 38bf0a49b3c4af6088fccf4aea54b5e624b67dd8 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:30:55 +0000 Subject: [PATCH 33/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20ConfirmDialog=20?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E7=A1=AE=E8=AE=A4=E5=90=8E=E7=AB=8B=E5=8D=B3?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E2=80=94=E2=80=94=E8=BF=9B=E5=BA=A6=E6=9D=A1?= =?UTF-8?q?=E4=B8=8D=E8=A2=AB=E9=81=AE=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/components/ConfirmDialog.jsx | 7 ++----- ui/App/views/Mods/components/LoadMods.jsx | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ui/App/components/ConfirmDialog.jsx b/ui/App/components/ConfirmDialog.jsx index 3e902068..10f02a20 100644 --- a/ui/App/components/ConfirmDialog.jsx +++ b/ui/App/components/ConfirmDialog.jsx @@ -10,11 +10,8 @@ function ConfirmDialog({title, content, isOpen, close, onSuccess}) { const confirm = () => { setIsLoading(true); - onSuccess() - .finally(() => { - close(); - setIsLoading(false); - }) + close(); + onSuccess?.(); } return ( diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index 254b5bd9..bf1bfdd4 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -118,8 +118,7 @@ const LoadMods = ({refreshMods}) => { }} onSuccess={() => { const data = loadModsData; - setLoadModsData(undefined); - setTimeout(() => loadMods(data), 0); + loadMods(data); }} /> From 1b6192469dbf346bf132cfb49e402596efcae611 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:34:39 +0000 Subject: [PATCH 34/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20ConfirmDialog=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20closeImmediately=20=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E2=80=94=E2=80=94=E4=BB=85=E5=85=B3=E9=97=AD=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E4=B8=8D=E7=AD=89=E5=BE=85=20Promise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/components/ConfirmDialog.jsx | 14 +++++++++++--- ui/App/views/Mods/components/LoadMods.jsx | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/App/components/ConfirmDialog.jsx b/ui/App/components/ConfirmDialog.jsx index 10f02a20..124a552d 100644 --- a/ui/App/components/ConfirmDialog.jsx +++ b/ui/App/components/ConfirmDialog.jsx @@ -3,15 +3,23 @@ import { useTranslation } from 'react-i18next'; import Modal from "./Modal"; import Button from "./Button"; -function ConfirmDialog({title, content, isOpen, close, onSuccess}) { +function ConfirmDialog({title, content, isOpen, close, onSuccess, closeImmediately}) { const { t } = useTranslation('common'); const [isLoading, setIsLoading] = useState(false); const confirm = () => { setIsLoading(true); - close(); - onSuccess?.(); + if (closeImmediately) { + close(); + onSuccess?.(); + } else { + onSuccess() + .finally(() => { + close(); + setIsLoading(false); + }) + } } return ( diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index bf1bfdd4..905ade9b 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -120,6 +120,7 @@ const LoadMods = ({refreshMods}) => { const data = loadModsData; loadMods(data); }} + closeImmediately={true} /> : From 24749f0c469f2fb203623d9e168ba1279d4d98d2 Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:40:13 +0000 Subject: [PATCH 35/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E6=A8=A1=E7=BB=84=E8=BF=9B=E5=BA=A6=E6=9D=A1=E6=97=81=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=8F=96=E6=B6=88=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/views/Mods/components/LoadMods.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index 905ade9b..eb6c0050 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -93,7 +93,12 @@ const LoadMods = ({refreshMods}) => { value: save.name }))} /> - +
    + + {showProgress && ( + + )} +
    {showProgress && (
    From b26dfaff5985af3a3072d736a52525ef5775870a Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 11:59:34 +0000 Subject: [PATCH 36/38] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E5=AD=98=E6=A1=A3?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=A8=A1=E7=BB=84=E5=A2=9E=E5=8A=A0=20Checkl?= =?UTF-8?q?ist=20=E5=8B=BE=E9=80=89=E8=A6=81=E5=AE=89=E8=A3=85=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/views/Mods/components/LoadMods.jsx | 108 +++++++++++++++++----- ui/locales/en/mods.json | 5 + ui/locales/zh-CN/mods.json | 5 + 3 files changed, 96 insertions(+), 22 deletions(-) diff --git a/ui/App/views/Mods/components/LoadMods.jsx b/ui/App/views/Mods/components/LoadMods.jsx index eb6c0050..ae4f6bbf 100644 --- a/ui/App/views/Mods/components/LoadMods.jsx +++ b/ui/App/views/Mods/components/LoadMods.jsx @@ -8,7 +8,7 @@ import Button from "../../../components/Button"; import modsResource from "../../../../api/resources/mods"; import modResource from "../../../../api/resources/mods"; import FactorioLogin from "./AddMod/components/FactorioLogin"; -import ConfirmDialog from "../../../components/ConfirmDialog"; +import Modal from "../../../components/Modal"; import socket from "../../../../api/socket"; const LoadMods = ({refreshMods}) => { @@ -22,6 +22,9 @@ const LoadMods = ({refreshMods}) => { const [loadModsData, setLoadModsData] = useState(undefined); const [installProgress, setInstallProgress] = useState({current: 0, total: 0}); const [showProgress, setShowProgress] = useState(false); + const [modList, setModList] = useState([]); + const [selectedMods, setSelectedMods] = useState(new Set()); + const [showModList, setShowModList] = useState(false); useEffect(() => { (async () => { @@ -54,30 +57,65 @@ const LoadMods = ({refreshMods}) => { }; }, []); - const loadModsRequested = data => { + const loadModsRequested = async data => { setIsLoading(true); + const result = await savesResource.mods(data.save); + const mods = result?.mods || []; + + if (mods.length === 0) { + window.flash(t('noModsFound', { ns: 'mods' }), "green"); + setIsLoading(false); + return; + } + + setModList(mods.filter(m => m.name !== 'base')); + setSelectedMods(new Set(mods.filter(m => m.name !== 'base').map(m => m.name))); setLoadModsData(data); + setShowModList(true); + setIsLoading(false); + } + + const toggleMod = (name) => { + const next = new Set(selectedMods); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + setSelectedMods(next); + } + + const selectAll = () => { + setSelectedMods(new Set(modList.map(m => m.name))); } - const loadMods = async data => { + const deselectAll = () => { + setSelectedMods(new Set()); + } + + const loadMods = async () => { + const data = loadModsData; + setShowModList(false); + setLoadModsData(undefined); + setIsLoading(true); + try { await modResource.deleteAll(); - const result = await savesResource.mods(data.save); - const mods = result?.mods || []; + const toInstall = modList.filter(m => selectedMods.has(m.name)); - if (mods.length === 0) { - window.flash(t('noModsFound', { ns: 'mods' }), "green"); + if (toInstall.length === 0) { + window.flash(t('noModsSelected', { ns: 'mods' }), "gray-light"); return; } - await modResource.portal.installMultiple(mods); + await modResource.portal.installMultiple(toInstall); refreshMods(); window.flash(t('modsLoaded').replace('{save}', data.save), "green"); } catch (e) { window.flash(t('errorOccurred', { ns: 'common' }), "red"); } finally { setIsLoading(false); - setLoadModsData(undefined); + setShowProgress(false); } } @@ -94,7 +132,7 @@ const LoadMods = ({refreshMods}) => { }))} />
    - + {showProgress && ( )} @@ -113,19 +151,45 @@ const LoadMods = ({refreshMods}) => {
    )} - { - setIsLoading(false); - setLoadModsData(undefined); - }} - onSuccess={() => { - const data = loadModsData; - loadMods(data); - }} - closeImmediately={true} + isOpen={showModList} + content={ +
    +

    {t('selectModsToInstall', { ns: 'mods' })}

    +
    + + +
    +
    + + + {modList.map(mod => ( + toggleMod(mod.name)}> + + + + + ))} + +
    + + {mod.name}{mod.version}
    +
    +
    + } + actions={ +
    + + +
    + } /> : diff --git a/ui/locales/en/mods.json b/ui/locales/en/mods.json index ab6c3761..a0c1e14b 100644 --- a/ui/locales/en/mods.json +++ b/ui/locales/en/mods.json @@ -31,6 +31,11 @@ "deleteExistingMods": "Delete all existing mods before loading? Existing mods will be permanently removed.", "noModsFound": "No mods found in this save.", "installingMods": "Installing mods", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "selectModsToInstall": "Select mods to install from this save:", + "installSelected": "Install ({count})", + "noModsSelected": "No mods selected.", "compatibility": "Compatibility", "modVersion": "Mod Version", "factorioVersion": "Factorio Version" diff --git a/ui/locales/zh-CN/mods.json b/ui/locales/zh-CN/mods.json index e789f612..3f4404ce 100644 --- a/ui/locales/zh-CN/mods.json +++ b/ui/locales/zh-CN/mods.json @@ -31,6 +31,11 @@ "deleteExistingMods": "加载前删除所有现有模组?现有模组将被永久移除。", "noModsFound": "此存档中没有模组。", "installingMods": "安装模组中", + "selectAll": "全选", + "deselectAll": "取消全选", + "selectModsToInstall": "选择要从此存档安装的模组:", + "installSelected": "安装 ({count})", + "noModsSelected": "未选择任何模组。", "compatibility": "兼容性", "modVersion": "模组版本", "factorioVersion": "Factorio 版本" From c497c95e28b395aca843ce4a4b389735b252d2ce Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 12:23:19 +0000 Subject: [PATCH 37/38] =?UTF-8?q?=E8=B0=83=E6=95=B4:=20=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E5=AE=BD=E5=BA=A6=E9=80=82=E9=85=8D=E6=89=8B=E6=9C=BA=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=A7=BB=E5=8A=A8=E7=AB=AF=2011/12=20=E5=AE=BD?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/App/components/Modal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/App/components/Modal.jsx b/ui/App/components/Modal.jsx index 021ed680..ae381248 100644 --- a/ui/App/components/Modal.jsx +++ b/ui/App/components/Modal.jsx @@ -11,7 +11,7 @@ const Modal = ({title, content, isOpen, actions = null}) => {
    From d1eb10a86221103685052e882d6ee0a404f8dfeb Mon Sep 17 00:00:00 2001 From: BAYUNZIYUE Date: Sun, 21 Jun 2026 14:02:14 +0000 Subject: [PATCH 38/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E9=94=81=E5=A2=9E=E5=8A=A0=2030=20=E7=A7=92=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E2=80=94=E2=80=94=E9=98=B2=E6=AD=A2=E6=AD=BB=E9=94=81=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E9=A1=B5=E9=9D=A2=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/factorio/mod_modInfo.go | 1 - src/lockfile/lockfile.go | 25 ++++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/factorio/mod_modInfo.go b/src/factorio/mod_modInfo.go index 6397aa0a..3e9225c4 100644 --- a/src/factorio/mod_modInfo.go +++ b/src/factorio/mod_modInfo.go @@ -144,7 +144,6 @@ func (modInfoList *ModInfoList) deleteMod(modName string) error { filePath := filepath.Join(modInfoList.Destination, mod.FileName) FileLock.LockW(filePath) - //delete mod err = os.Remove(filePath) FileLock.Unlock(filePath) if err != nil { diff --git a/src/lockfile/lockfile.go b/src/lockfile/lockfile.go index 6e7b653d..7162b076 100644 --- a/src/lockfile/lockfile.go +++ b/src/lockfile/lockfile.go @@ -107,33 +107,36 @@ func (fl *FileLock) RUnlock(filePath string) error { return nil } -func (fl *FileLock) LockW(filePath string) { +func (fl *FileLock) LockW(filePath string) error { + deadline := time.Now().Add(30 * time.Second) for { err := fl.Lock(filePath) if err == ErrorAlreadyLocked { + if time.Now().After(deadline) { + log.Printf("file lock timeout for %s, giving up", filePath) + return errors.New("file lock timeout") + } time.Sleep(time.Second * 2) - log.Println("file locked wait two seconds to access write-lock") } - if err == nil { - break + return nil } } - return } -func (fl *FileLock) RLockW(filePath string) { +func (fl *FileLock) RLockW(filePath string) error { + deadline := time.Now().Add(30 * time.Second) for { err := fl.RLock(filePath) - if err == ErrorAlreadyLocked { + if time.Now().After(deadline) { + log.Printf("file read-lock timeout for %s, skipping", filePath) + return errors.New("file read-lock timeout") + } time.Sleep(time.Second * 2) - log.Println("file locked ... wait two seconds to try to access read-lock") } - if err == nil { - break + return nil } } - return }