Conversation
Translated catalogs drift silently: a key added to one locale file and forgotten in another only surfaces at runtime as a missing string. Add a script that flattens every locale directory under src/i18n/locales against the zh reference (i18next plural suffixes like _one/_other are stripped before comparing, since those differ per locale by design) and fails on any missing key, unexpected key, or missing file. The script discovers locale directories dynamically instead of hardcoding a list, so it keeps working as locales are added or removed. Wire it into the Docker image build ahead of the Next.js build so a drifted catalog fails CI instead of shipping.
next.config.ts sets output: 'export', so there is no server to negotiate the locale and next-intl would require prefix routing that changes every URL. i18next runs in the browser instead and routing is untouched. The instance is pinned to the zh fallback at init so the prerendered HTML matches on hydration; the detected locale is applied after mount by I18nProvider, which also renders the document title from the active locale so it stays correct across route changes instead of racing the static metadata title that next/head would otherwise own. Only the common namespace exists yet; the feature screens add their own namespaces to the resources map as they migrate.
… a language switcher The attribute was hardcoded to zh-CN, so screen readers and browser page translation misread every non-Chinese page. Unlike page content, this one attribute can be fixed before paint, so it follows the existing theme-script.tsx pattern: an inline script run from <head> reads the stored choice and the navigator languages, in the same candidate order detectLocale() uses on the client, and sets the attribute before React hydrates. zh keeps the zh-CN region tag; en has no region variant here. The switcher sits beside the theme toggle in the header and persists to localStorage under the same naming convention. It reads its options from SUPPORTED_LOCALES, so it grows with the catalog instead of needing a matching edit here. Language names stay written in their own language so a user stranded in an unreadable locale can still find their way back. The aria-label goes on SelectTrigger, the actual focusable combobox button, not on the SelectValue span nested inside it, which is never a button's accessible name. top-nav.tsx's nav items, role label and third-party-app dialog move onto the common catalog added in the previous commit.
First namespaces through the new i18n pipeline: login and logs, plus the small components several screens share (date range filter, image lightbox, theme toggle, version dialog, version check hook). The zh catalogs copy the original strings verbatim so nothing changes for existing zh users. request.ts sends the active i18n language as Accept-Language on every request and renders its generic failure message through the catalog too, so a backend error without a specific message still comes back in the reader's language.
Add the image-manager and debug namespaces and wire them into image-manager/page.tsx and the six panels under app/debug. The debug skill panel's two SKILL.md templates (one Chinese, one English) are fixed bilingual content that must keep rendering in their own language regardless of the dashboard's UI locale, not strings that should translate with it, so they are read via i18n.getFixedT against their own locale file instead of the active-locale t().
Adds the image namespace and wires it into the image page shell and its composer, results and sidebar components. zh copies the original strings verbatim.
Adds the accounts namespace and wires it into the accounts page and the account import dialog. Account status arrives from the API as a Chinese literal (正常/限流/异常/禁用) that backend business logic compares against directly, so the wire value stays untouched. account-status.ts maps each literal to a catalog key and only the display goes through it.
Adds the settings namespace, by far the largest catalog, and wires it into every settings card (core config, API docs, user keys, proxy, backup, WebDAV image storage, CPA pools, import browser, sub2api connections, third-party apps) plus the page shell and the Zustand store's toasts. This completes the web UI: no Chinese literal remains outside the zh catalogs. Endpoint paths, JSON field names and example curl payload content in api-docs-card stay byte-identical across locales, since translating them would hand a reader a command that fails when pasted. store.ts calls i18n.t() directly with an explicit ns option instead of useTranslation(), since it is a Zustand store rather than a component and has no hook to call.
The backend receives real HTTP headers, so it can negotiate a locale
directly from Accept-Language instead of depending on the browser to
relay one. A ContextVar in the new utils/i18n module holds the current
request's locale; t(key) resolves against it and falls back to zh, so
callers that do not thread a locale through keep producing the exact
output they always have.
LocaleMiddleware is a plain ASGI callable rather than
@app.middleware("http") (BaseHTTPMiddleware), which spawns the
downstream app in a separate task and pumps every response chunk
through a zero-capacity anyio stream. The primary path here is
token-by-token SSE, so that would add two task hops per chunk of every
streamed response just to localise a handful of error strings.
LocaleMiddleware runs in the request's own task and reads
Accept-Language straight from the ASGI scope.
Only strings that actually reach a client are touched; every one of the
localised exception sites was traced to confirm it is reachable from a
request. Chinese comments and log lines are left alone to keep the
diff reviewable. A few backup_service.py sites are also reachable from
the backup scheduler thread, which has no request in scope and
correctly falls back to zh there.
re_login_accounts runs on a detached thread polled by a later, separate
request, so it cannot bake a translated sentence into its stored
progress: that would freeze the writer's locale regardless of who reads
it later. It now stores a message key plus an error_is_key marker, and
_render_relogin_results in api/accounts.py renders that key against the
polling request's own locale at read time. The AccountStatus wire
values in the same records (跳过/异常/...) stay untouched since backend
business logic compares against them directly; only the accompanying
error text is localised.
The admin session's display name and the default names given to newly
created auth keys also move onto the catalog: top-nav.tsx renders
`roleLabel · displayName`, so a non-zh session showed a translated role
badge next to a raw Chinese name.
Also fixes a pre-existing UnboundLocalError in re_login_accounts: a
local `t` (threading.Thread) bound later in the function body shadowed
the module-level `t` (utils.i18n.t) for the entire function, including
the earlier unknown-account branch, aborting the whole re-login batch
whenever it ran.
README.md keeps its Chinese content and stays the default; the only change to it is the new language navigation line at the top. Commands, paths, environment variables and endpoint identifiers are byte-identical across both files, so a reader can paste from either one.
Adds the vi locale directory (all eight namespaces) and declares vi as a supported locale everywhere the zh/en list was hardcoded: locale-script.tsx's before-paint script, i18n/config.ts's SUPPORTED_LOCALES and resources map, and utils/i18n.py's SUPPORTED_LOCALES and MESSAGES catalog. The language switcher, the Accept-Language negotiation and every consumer of SUPPORTED_LOCALES already read this list generically, so no other code changes. This is deliberately the first commit that depends on Vietnamese: the zh+en state up to this point is a complete, working product on its own, and everything from here to the end of the branch exists only to add Vietnamese on top of it.
Translated from the Chinese original rather than from the English, so the two translations do not compound each other's drift. Structure, code blocks and every identifier match the other two files. The language navigation line in README.md and README.en.md now links to all three.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds English and Vietnamese to the dashboard, the client-facing API messages and the README, alongside the existing Chinese.
Existing Chinese users see no change. The fallback locale is
zh, neveren, and everyzhcatalog value is the original string copied byte-for-byte. A request with noAccept-Language, a browser that reports no supported language, and a client with JavaScript disabled all get exactly what they get today.What is in here
Accept-Language.README.en.mdandREADME.vi.md, plus a one-line language bar at the top of all three.Design decisions worth knowing before reviewing
Client-side i18next, not
next-intl.web/next.config.tssetsoutput: 'export', so there is no server at runtime, no middleware, and no server-side locale negotiation. Per its own docs,next-intlunder a static export requires prefix routing (/zh/,/en/,/vi/), which would change every URL and triple the static output. i18next runs in the browser instead and routing is untouched.The instance is pinned to the
zhfallback at init so the prerendered HTML matches on hydration; the detected locale is applied after mount. The visible cost is one frame of Chinese on first load, which is inherent to baking page text into static HTML.Account status values stay Chinese.
AccountStatus = "正常" | "限流" | "异常" | "禁用"is a wire format:account_service.py,model_service.pyandeditable_file_task_service.pycompare against those literals. Only the rendered label is translated, through a key map inweb/src/lib/account-status.ts. The same split applies to the changelog category badges.Re-login progress renders in the reader's language, not the writer's. That worker runs in a detached thread with no request context, and its records are polled later by a different request. Storing a translated sentence would freeze the writer's locale, so the worker stores a message key and the polling endpoint renders it against the reader's
Accept-Language.The browser tab title renders from a client component. Static
metadatais evaluated at build time and cannot read a client-side locale. An imperativedocument.titleassignment loses a race with React reconciling the<title>element, on hydration and on every navigation. Rendering the element instead makes React the owner. The prerendered HTML still carries the Chinese title.Backend locale comes from a pure ASGI middleware, not
BaseHTTPMiddleware, so token-by-token SSE responses pay no per-chunk overhead.If you do not want Vietnamese
The last two commits are Vietnamese-only. Drop them and you have a working Chinese-plus-English branch: I verified at that exact commit that
npm run check:locales,npm run buildandtsc --noEmitall pass with two locales.What is deliberately still Chinese
Ten files under
web/srchold Chinese outside thezhcatalogs, all on purpose: the language self-name中文; the fixed-language SKILL.md templates in the debug panel, which are meant to be shown side by side in Chinese and English; theAccountStatuswire values; comparison literals; and code comments. Python comments and log lines are untouched throughout.Three things I would rather flag than hide
settings/components/proxy-settings.tsxandproxy-settings-card.tsxare not imported anywhere. They were translated because they are part of the settings tree, which adds 108 catalog entries nothing renders, and is why a few proxy strings exist in triplicate. Happy to delete both components and their sections if you confirm they are dead.npm run check:localesnow runs inside the Docker build, so a catalog mismatch fails the image build. The only workflow is tag-triggered, so that check first fires at release time. A smallpull_requestjob running the same command would move the failure earlier. I did not add one because touching CI felt out of scope for a translation PR.docs/feature-status.en.mdcarries an.ensuffix but is written in Chinese, and all three READMEs link to it. That predates this PR. An English reader following that link lands on a Chinese page. Renaming it tofeature-status.mdand adding real translations would fix it, but renaming one of your files inside a translation PR seemed presumptuous, so I left it and am asking instead.Verification
npm run check:locales,npm run build,npx tsc --noEmit: clean.