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/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`,在 `` 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 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 ` { i18n.changeLanguage(e.target.value); }} + > + + +
- +
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/FactorioLogin.jsx b/ui/App/views/Mods/components/AddMod/components/FactorioLogin.jsx index 7450333e..193d2885 100644 --- a/ui/App/views/Mods/components/AddMod/components/FactorioLogin.jsx +++ b/ui/App/views/Mods/components/AddMod/components/FactorioLogin.jsx @@ -10,9 +10,9 @@ const FactorioLogin = ({setIsFactorioAuthenticated}) => { const {register, handleSubmit} = useForm(); const [isLoading, setIsLoading] = useState(false); - const login = ({username, password}) => { + const login = ({username, token}) => { setIsLoading(true); - modsResource.portal.login(username, password) + modsResource.portal.login(username, token) .then(res => { setIsFactorioAuthenticated(true) }) @@ -28,8 +28,8 @@ const FactorioLogin = ({setIsFactorioAuthenticated}) => {
    -
    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/api/resources/mods.js b/ui/api/resources/mods.js index 33d74a78..8d35521d 100644 --- a/ui/api/resources/mods.js +++ b/ui/api/resources/mods.js @@ -34,10 +34,10 @@ const mods = { }, downloadAllURL: '/api/mods/download', portal: { - login: async (username, password) => { + login: async (username, token) => { const response = await client.post('/api/mods/portal/login', { username, - password + token }); return response.data; }, 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..88ac9683 --- /dev/null +++ b/ui/locales/en/mods.json @@ -0,0 +1,35 @@ +{ + "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.", + "compatibility": "Compatibility", + "modVersion": "Mod Version", + "factorioVersion": "Factorio Version" +} 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" +} 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..30aaef2c --- /dev/null +++ b/ui/locales/zh-CN/mods.json @@ -0,0 +1,35 @@ +{ + "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": "加载前删除所有现有模组?现有模组将被永久移除。", + "compatibility": "兼容性", + "modVersion": "模组版本", + "factorioVersion": "Factorio 版本" +} 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": "修改" +}
    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' })}