Skip to content

feat(i18n)!: Russian/English across the panel, bots, subscription page and CLI - #41

Merged
AppsGanin merged 1 commit into
mainfrom
feat/i18n
Jul 29, 2026
Merged

feat(i18n)!: Russian/English across the panel, bots, subscription page and CLI#41
AppsGanin merged 1 commit into
mainfrom
feat/i18n

Conversation

@AppsGanin

Copy link
Copy Markdown
Owner

Every surface a person reads is now bilingual. Four language sources, each
chosen because it is the only thing that surface actually knows:

panel localStorage, else navigator.languages (per browser)
sub page Accept-Language
client bots Telegram language_code, stored per subscriber
admin bot a panel-wide setting (pushes have no update to read from)
CLI English only

Frontend uses react-i18next with typed dictionaries: ru.ts exports
type Dict = typeof ru and en.ts is declared const en: Dict, so a key in
one and not the other is a tsc error rather than a silent runtime fallback.

Backend sends a KEY plus arguments, never rendered prose — the panel's
language is a per-browser choice the server cannot see. Validation and API
errors travel as {error, code, args}: the panel words the code and falls back
to the message, so a build whose dictionaries lag the server still reads as a
sentence instead of "err.tokenRequired". Health checks, journal catalogs and
audit sections work the same way. Verbatim third-party text (an Xray config
error, an ACME failure) stays free-form, carried in args.detail.

Diagnostics that no localised surface shows — the updater, blocklist fetch,
provider API errors, the SSH install log — are English rather than translated.
They land in logs and in the external REST API, where there is no dictionary.

Guards, because a key is a string resolved at runtime and nothing else can see
a typo in one:

  • PluralComplete makes a missing plural form a compile error. English
    selects only one/other while Russian needs one/few/many, and the first cut
    of this shipped English counts rendering in Russian.
  • TestEveryCallSiteKeyExists walks every i18n.T/TN literal key in the repo.
  • internal/i18n/dictcheck resolves a dotted key through the dictionary
    nesting, so a leaf under the wrong parent fails. Substring matching had
    accepted nodes.update for audit.sec.update.
  • Per-package parity tests for every code-carrying writer.
  • TestAcceptLanguageAgreesWithNormalize pins the surfaces to one answer: a
    German reader got English from the bots and Russian from the page.

Stock plan names and the node-0 display name become English (Free, Trial,
Standard, Master) — they are shipped defaults, and a migration renames them
only where the operator has not.

All code comments are English.

BREAKING CHANGE: response shapes that carried rendered Russian now carry
dictionary keys, so anything reading them has to word them itself.

  • GET /api/health — HealthCheck.label → label_key, hint → hint_key, and
    detail splits into detail_key + args (detail survives, but only for
    verbatim third-party text such as an Xray config error).
  • GET /api/settings/abuse — FileInfo.title → title_key.
  • The event and webhook catalog endpoints return [{key}] instead of
    [{key,label}]; the label is gone.
  • AdminAudit.target for a settings row now holds an "audit.sec.*" key
    instead of a Russian section name. Rows written before this release keep
    their old text and are shown verbatim.
  • Validation errors gain "code" and "args" alongside "error". The field is
    additive, but many "error" strings changed wording or language.
  • The three stock plans are renamed to Free/Trial/Standard and node 0 to
    "Master", by migration, only where the operator has not renamed them.

Release-As: 2.0.0

…e and CLI

Every surface a person reads is now bilingual. Four language sources, each
chosen because it is the only thing that surface actually knows:

  panel         localStorage, else navigator.languages (per browser)
  sub page      Accept-Language
  client bots   Telegram language_code, stored per subscriber
  admin bot     a panel-wide setting (pushes have no update to read from)
  CLI           English only

Frontend uses react-i18next with typed dictionaries: ru.ts exports
`type Dict = typeof ru` and en.ts is declared `const en: Dict`, so a key in
one and not the other is a tsc error rather than a silent runtime fallback.

Backend sends a KEY plus arguments, never rendered prose — the panel's
language is a per-browser choice the server cannot see. Validation and API
errors travel as {error, code, args}: the panel words the code and falls back
to the message, so a build whose dictionaries lag the server still reads as a
sentence instead of "err.tokenRequired". Health checks, journal catalogs and
audit sections work the same way. Verbatim third-party text (an Xray config
error, an ACME failure) stays free-form, carried in args.detail.

Diagnostics that no localised surface shows — the updater, blocklist fetch,
provider API errors, the SSH install log — are English rather than translated.
They land in logs and in the external REST API, where there is no dictionary.

Guards, because a key is a string resolved at runtime and nothing else can see
a typo in one:
  - PluralComplete<T> makes a missing plural form a compile error. English
    selects only one/other while Russian needs one/few/many, and the first cut
    of this shipped English counts rendering in Russian.
  - TestEveryCallSiteKeyExists walks every i18n.T/TN literal key in the repo.
  - internal/i18n/dictcheck resolves a dotted key through the dictionary
    nesting, so a leaf under the wrong parent fails. Substring matching had
    accepted `nodes.update` for `audit.sec.update`.
  - Per-package parity tests for every code-carrying writer.
  - TestAcceptLanguageAgreesWithNormalize pins the surfaces to one answer: a
    German reader got English from the bots and Russian from the page.

Stock plan names and the node-0 display name become English (Free, Trial,
Standard, Master) — they are shipped defaults, and a migration renames them
only where the operator has not.

All code comments are English.

BREAKING CHANGE: response shapes that carried rendered Russian now carry
dictionary keys, so anything reading them has to word them itself.

  * GET /api/health — HealthCheck.label → label_key, hint → hint_key, and
    detail splits into detail_key + args (detail survives, but only for
    verbatim third-party text such as an Xray config error).
  * GET /api/settings/abuse — FileInfo.title → title_key.
  * The event and webhook catalog endpoints return [{key}] instead of
    [{key,label}]; the label is gone.
  * AdminAudit.target for a settings row now holds an "audit.sec.*" key
    instead of a Russian section name. Rows written before this release keep
    their old text and are shown verbatim.
  * Validation errors gain "code" and "args" alongside "error". The field is
    additive, but many "error" strings changed wording or language.
  * The three stock plans are renamed to Free/Trial/Standard and node 0 to
    "Master", by migration, only where the operator has not renamed them.

Release-As: 2.0.0
@AppsGanin
AppsGanin merged commit 3de8b52 into main Jul 29, 2026
4 checks passed
@AppsGanin
AppsGanin deleted the feat/i18n branch July 29, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant