Skip to content

feat(i18n): spec 019 foundation: language packs, checker, problem+json, dashboard runtime - #668

Draft
ValgulNecron wants to merge 39 commits into
masterfrom
claude/project-thread-awjqv0
Draft

ValgulNecron wants to merge 39 commits into
masterfrom
claude/project-thread-awjqv0

Conversation

@ValgulNecron

Copy link
Copy Markdown
Collaborator

Before: Gameplane has no translation system. Dashboard text, API errors and notifications are English-only, and dates and numbers are hard-coded to en-US.

After: Gameplane has one shared catalog and its checks, still behind an English-only catalog. A new i18n/ Go module holds the language packs, an ICU MessageFormat formatter, a pack validator and the i18n-check CI tool. The API returns RFC 9457 problem details with a messageId only to clients that ask for application/problem+json. The dashboard has a react-intl runtime: language resolution, a no-flash <html lang> boot snippet and locale-aware formatting. Nothing visible changes in English, and plain-text API error bodies stay byte-identical.

This is the foundation (Phases 1–2) of spec 019, tasks T001–T034 plus T048. The pickers and the per-story work come in later PRs.

How

  • i18n/: icu (parser for the ICU subset, CLDR plurals via golang.org/x/text), pack (load, validate with the finding codes from contracts/check-tool.md, completeness, 80% listing rule), catalog (embedded packs, regional → base → English fallback, invalid translations dropped), cmd/i18n-check and one seeded defective fixture per finding code (SC-007). Coverage gate 90; the module is added to go.work, GO_MODULES, the CI matrices and dependabot.
  • API: api/internal/messages registry (server.* ids with translator notes). httperr.Message and WriteMessage. Content negotiation parses Accept q-values, and wildcards never select problem details. 5xx responses never echo error text or args. make i18n-sync writes the registry into i18n/locales/en/app.json.
  • Web: src/lib/i18n/ (provider, resolution order FR-004, storage, format helpers, server-message mapping). APIError parses problem bodies but keeps err.body as the English detail, so existing callers are unchanged. New scripts i18n:extract, i18n:compile and i18n:manifest. The formatjs ESLint rules are scoped to migrated paths. The Dockerfile builds the language manifest in a Go stage.
  • CI: the web job builds the manifest and runs i18n-check, which checks extraction drift and server-message drift and writes before/after completeness to the job summary. It also self-tests the checker on the fixtures. make check-i18n joins make lint.

Spec notes

  • --registry <file> was added to i18n-check for the two server flags, and contracts/check-tool.md is updated to match.
  • go:embed lives in the root i18n package because embed paths cannot use ...

Only compile checks and the generators were run locally (go build, tsc --noEmit, npm run build, make i18n-sync). Tests and lint run here in CI.

🤖 Generated with Claude Code


Generated by Claude Code

… and i18n-check

Spec 019 T001-T004, T007-T019: new Go module i18n/ (go.work, GO_MODULES,
CI matrices, dependabot, coverage gate 90), ICU MessageFormat subset
parser/formatter with CLDR plurals, pack load/validate/completeness,
embedded catalog with regional -> base -> en fallback, the i18n-check CLI
and one seeded defective fixture per finding code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Spec 019 T005, T006 (web), T024-T033: react-intl provider with language
resolution, storage and no-flash boot snippet; locale-aware formatting
helpers replacing hard-coded en-US; problem+json parsing in APIError with
err.body kept as the English detail; extract/compile/manifest scripts;
formatjs ESLint rules on migrated paths. No visible UI change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Spec 019 T006 (api/operator), T020-T023: api/internal/messages registry
(server.* ids with translator notes), httperr.Message and WriteMessage,
RFC 9457 problem details only when Accept lists application/problem+json
with q > 0 (plain-text bodies stay byte-identical), 5xx never echo error
text or args, messages-export command and make i18n-sync writing the
server.* entries into i18n/locales/en/app.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Spec 019 T034, T048: the web job builds the language manifest, runs
i18n-check (extraction drift, server-message drift, before/after
completeness in the job summary) and self-tests the checker on the
seeded defective packs; make check-i18n joins make lint. i18n-check now
creates the manifest's directory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@ValgulNecron ValgulNecron added area: api REST/WebSocket gateway area: shared netguard, gameaction, gameproto, svcutil and other shared packages area: specs spec-kit feature specs and design docs area: web React dashboard type: feature New capability or user-facing addition labels Oct 11, 2026 — with Claude
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ValgulNecron ValgulNecron self-assigned this Oct 11, 2026
…tions

golangci-lint (gosec G703, staticcheck QF1001) on the new i18n module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
eslint formatjs/enforce-description on src/lib/i18n/serverMessage.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

CI Report - 2ec9b39

run 38323667797 - attempt 1

1 failed - 29 passed - 0 skipped - 97m44s elapsed

Failing

Job Log
go logs

Coverage

Module Measured Gate Margin
web L95.61 S94.17 B88.9 F91.92 L92 S92 B82 F76 +2.17

Changed

Area Files +/-
i18n/ 75 +6034/-0
api/ 55 +3410/-209
operator/ 34 +3109/-40
web/ 47 +2887/-103
(root) 7 +2521/-119
assets/ 36 +2197/-114
gp-module/ 20 +1283/-8
test/ 8 +708/-8
specs/ 8 +216/-82
docs/ 6 +276/-2
charts/ 18 +164/-18
.github/ 3 +74/-2
hack/ 2 +53/-4
.devcontainer/ 1 +1/-1

Slowest jobs

Job Duration
e2e telemetry / amd64 (kind) 23m13s
e2e telemetry / arm64 (kind) 22m3s
e2e capture overhead (kind) 13m29s
build e2e images 12m19s
e2e multicluster / amd64 (kind x2) 9m36s

e2e buckets run

api-agent, api-auth, api-mods, api-rbac, api-roles, bot-fast, multicluster, operator, ratelimit, telemetry, upgrade

Copy link
Copy Markdown
Collaborator Author

web e2e (mock) fails in web/e2e/specs/api-cache.spec.ts:68. The test expects API reads to send exactly Accept: application/json, but this PR sends Accept: application/json, application/problem+json, which spec 019 contracts/server-messages.md §1 requires. The test exists to check that API reads never ask for HTML, and that still holds. Only the exact header string changed. Updating the test's expected value needs the maintainer's sign-off, which has been asked for in the project thread. Until then this check stays red.


Generated by Claude Code

claude added 12 commits October 12, 2026 00:03
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
…guage

Spec 019 T036, T038-T041, T093: migration 016 adds user_preferences.language;
PUT /users/me/language (listed languages only, server.i18n.unknown_language);
language in GET /users/me/preferences; unauthenticated GET /auth/locale
exposing only defaultLanguage; admin config section i18n validated against
the listed languages (server.i18n.unlisted_language). E2E for /auth/locale
in the api-auth bucket.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
…ider

Spec 019 T042, T046, T047: Users.setLanguage and Auth.locale endpoints,
AppI18nProvider reading the cached /users/me language and /auth/locale
default, and the 0% fixture language behind VITE_I18N_FIXTURES=1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

GameServerStatus gains conditionArgs (map of condition type to the values
interpolated into its English message), set after each status pass and
cleared when the reason carries no values. The condition-ids generator
writes condition.<type>.<reason> English entries into the reference pack
and has a -check mode for drift.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Sinks take an optional language (validated against the listed languages).
Titles, details and test text come from notify.* catalog entries in the
sink language, then the instance default, then English. Webhook payloads
add language, title and text and keep every existing field. English output
is unchanged.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Every 4xx http.Error in handlers, auth, ws and rbac now goes through
httperr.WriteMessage (registry id) or httperr.WriteCode (generic id).
Plain-text bodies stay byte-identical; 5xx stays http.Error. Unauthenticated
login and OIDC paths only use server.auth.invalid_credentials or generic
ids. make i18n-sync also syncs notify.* and condition.* ids, and
i18n-check covers notify.* drift. Adds the problem+json E2E and ticks the
019 US5 backend tasks.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

i18n-check gains --valid-out, which writes each pack's usable entries.
The compile script and the web image use it, so a broken translation
shows the English text instead (FR-018).

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
valgul approved this expectation change on the spec 019 decision card.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

New i18n/modlocale derives module ids from module.yaml and template.yaml
with reversible ~0/~1 escaping. gp-module gains i18n extract and i18n
stamp, validate checks locales/*.json, and package adds one OCI layer per
language and includes locales/ in --output tarballs. i18n-check --module
checks en.json drift and validates translations.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Every module source (OCI, directory, git, HTTP, upload) carries
locales/*.json. The Module reconciler validates them against the template's
ids and writes valid entries to <template>-locales in the operator
namespace. GameTemplate status gains conditions; LocalesValid reports
WriteFailed, LocalesTooLarge (naming oversized languages) or
InvalidEntries, and the Warning event fires only when problems change.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

GET /modules/{name}/locales/{tag} returns the module's messages for the
tag, its base language, or {} (also for a malformed tag). Uploads may
include locales. Records valgul's US6 rulings as OD-005..OD-014.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Each locale file was capped at 900 KiB but a bundle could carry any
number of them. Languages past the 4 MiB budget are reported as
oversized (LocalesTooLarge), the same as an oversized file.

Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

valgul ruled on OD-007 (report in validate), OD-009 (WriteFailed ranks
first) and OD-013 (i18n-check --module --stamp stays an error).

Signed-off-by: Claude <noreply@anthropic.com>
ValgulNecron pushed a commit to GameplanePanel/module that referenced this pull request Oct 12, 2026
The locale check needs GameplanePanel/Gameplane#668 on master; running
it last keeps the build.sh and validate.py tests visible meanwhile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
ValgulNecron and others added 2 commits October 12, 2026 21:55
Design for T043, T072 and T094 of spec 019, with exports.

- Personal settings: a Language card is the first card of Theme &
  Appearance (also on the Warnings variant), plus a mobile screen.
- Login and share pages: a compact picker in the top-right corner of
  the card on every state, and a screen with its menu open. It shows
  language names only.
- Notifications: a Language select on each sink row and a new screen
  for the Add sink form, which the design did not have.
- Admin settings: a Language card with the Default language field.

Records the placement rulings as OD-015 to OD-017 and ticks the three
design tasks. loginCard (J14ME) is unchanged: Pencil cannot insert a
child into it, so the login screens use a wrapper component (see
assets/design-export/MANIFEST.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com>
PR #668 conflicted with master, so no CI run started. Four conflicts,
one hunk each, resolved by keeping both sides:

- Makefile: lint runs check-i18n and check-coverage-gaps.
- api/go.mod: golang.org/x/sys v0.49.0 from master; golang.org/x/text
  stays a direct requirement.
- web/package.json and web/package-lock.json: the @formatjs dev
  dependencies from the branch, @playwright/test ^1.64.0 from master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com>

Copy link
Copy Markdown
Collaborator Author

web e2e (mock) fails on c0f0aba in two tests: serverDetail.spec.ts:35 and settingsSubTabs.spec.ts:50. Both assert that no console errors appear, and both see Failed to load resource: ... 502 (Bad Gateway).

Cause: this PR's AppI18nProvider requests GET /auth/locale. The browser MSW handlers in web/src/test/handlers.ts have no route for it, so the request falls through to the dev proxy, which has no backend and returns 502. The fix is a one-line mock route in the default and screenshot handler sets. That changes test setup, so it waits for the maintainer's sign-off. No assertion changes.


Generated by Claude Code

…lgul)

The dashboard asks for the instance default language on every page load.
Without a mock route the request reached the dev proxy and returned 502,
which failed the console-error checks in serverDetail.spec.ts and
settingsSubTabs.spec.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
…approved by valgul)

--module now checks en.json against the ids the template derives (OD Q4),
so the fixture template declares spec.displayName and the packs use the
derived id template.displayName. Both cases and exit codes are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
hadolint DL3062 flags go run without a version; building the local
package and running the binary does the same work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
…ad-awjqv0

Signed-off-by: Claude <noreply@anthropic.com>
Codegen drift gate (make tidy) expects the tidied i18n/go.sum.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

docs and repository checks fails on 45c29e4 at make check-coverage-gaps. The error is specs/013-expand-test-coverage/coverage-gaps.md:3: Summary closed count is 3, table has 4 (and open 18 vs 17).

This isn't caused by this PR: it doesn't touch that file, and master 2155568 has the same mismatch. #672 and #674 each flipped a gap against the same old summary line. No fix exists yet. I've asked the spec 013 thread, which owns the file, to fix it on master. I'll merge master here once the fix lands.


Generated by Claude Code

golangci-lint: errcheck on the i18n command's output writes, gosec G602
on the --output value read, and G703 on the stamp write. Translations
are now written through an os.Root on the locales directory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
gosec G703 on the condition-ids generator's write of app.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

web is red on b1b2508 because CaptureWidget.test.tsx › "enables download button only for Completed captures" failed at line 798.

The test races the capabilities query. It asserts the Completed row's download button is enabled right after the list renders, but canDownload stays false until capabilities resolves. This PR's only change to CaptureWidget.tsx swaps toLocaleString() for fmt.number(), which doesn't touch the disable logic. The same test passed on this PR's earlier runs.

A fix already exists on #677 (a8ffe53, which wraps the assertion in waitFor). It isn't ported here because this repo needs explicit sign-off before any test edit. It will reach this PR when #677 merges and master is merged in.


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
- gp-module: locale archive fixture includes the required README.md
- api/db: theme migration test applies the remaining migrations before
  reading preferences (016 adds language)
- api/handlers: TestValidateI18n checks the canonical JSON; the locales
  test path-escapes its invalid tag so the request reaches the handler
- e2e: close the locales response body; drop two unused ct assignments

Each change approved by valgul on its decision card.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
misspell flags the French "Exemple" as a typo of "Example". The
fixture pair becomes Template/Modèle; the test still compares message
ids only. Approved by valgul.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

go (operator) is red on both architectures because TestGameServer_ReleasedAddressBecomesAvailable times out (gameserver_envtest_test.go:3805).

The cause is in this PR. The new TestGameServer_ConditionArgsAddressInUse holds 203.0.113.20, the same address the existing test uses. Address conflicts are checked cluster-wide, and envtest never deletes namespaces, so the leftover holder keeps the other test reporting AddressInUse.

Fixing it means editing the new test. This repo needs explicit sign-off for test edits, and one option (a different IP) was declined. The fix is waiting on a decision in the project thread.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: api REST/WebSocket gateway area: shared netguard, gameaction, gameproto, svcutil and other shared packages area: specs spec-kit feature specs and design docs area: web React dashboard type: feature New capability or user-facing addition

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants