Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d239616
feat(i18n): add shared i18n module with ICU formatter, pack validator…
claude Oct 11, 2026
28bbca5
feat(web): add dashboard i18n runtime behind an English-only catalog
claude Oct 11, 2026
87b9655
chore(web): keep the MSW worker script at master's version
claude Oct 11, 2026
59cc486
feat(api): add server message registry and opt-in problem+json errors
claude Oct 11, 2026
04a4f8b
ci(i18n): check language packs, drift and fixtures in the web job
claude Oct 11, 2026
a57baca
chore(web): restore tsconfig.tsbuildinfo to master's version
claude Oct 11, 2026
805828a
docs(specs): tick 019 foundation tasks T001-T034 and T048
claude Oct 11, 2026
4e9a33d
fix(i18n): write packs through os.Root and apply De Morgan simplifica…
claude Oct 11, 2026
372eb9c
fix(web): give the server-message lookup a translator description
claude Oct 11, 2026
6b655ad
fix(i18n): fall back to the other branch for an unmatched select value
claude Oct 12, 2026
4ecccd3
feat(api): save each user's language and add the instance default lan…
claude Oct 11, 2026
cc34e8c
feat(web): feed the user's and instance's language into the i18n prov…
claude Oct 11, 2026
ef03b1f
docs(specs): tick 019 US1 backend and US7 config tasks
claude Oct 11, 2026
da2cc4e
feat(operator): publish condition args and condition.* message ids
claude Oct 12, 2026
acf94b6
feat(api): render notifications in each destination's language
claude Oct 12, 2026
8416427
feat(api): localizable 4xx errors through the message registry
claude Oct 12, 2026
0e03ed1
feat(i18n): drop invalid pack entries from the dashboard build
claude Oct 12, 2026
7d7e26a
ci: label translation-only PRs as docs
claude Oct 12, 2026
8c53dc5
docs: add the translator guide and tick 019 US2 tasks
claude Oct 12, 2026
38ade2f
test(web): expect the problem+json Accept header on API reads
claude Oct 12, 2026
70a9bed
feat(gp-module): derive, validate, stamp and package module translations
claude Oct 12, 2026
8a29c23
feat(operator): publish module translations to a locales ConfigMap
claude Oct 12, 2026
661167e
feat(api): serve module translations and tick 019 US6 backend tasks
claude Oct 12, 2026
a01d6fb
fix(operator): cap total module translation reads at 4 MiB
claude Oct 12, 2026
94a0101
feat(web): pseudo-language en-XA behind VITE_I18N_PSEUDO
claude Oct 12, 2026
f52da42
docs: describe localization in architecture docs and the changelog
claude Oct 12, 2026
37519b6
feat(gp-module): report YAML names that cannot become translation ids
claude Oct 12, 2026
ad4e684
feat(design): 019 language pickers and settings fields
ValgulNecron Oct 12, 2026
c0f0aba
chore: merge master into claude/project-thread-awjqv0
ValgulNecron Oct 12, 2026
585b752
test(web): mock GET /auth/locale in browser mock mode (approved by va…
claude Oct 12, 2026
aa73103
test(i18n): TestModulePacks fixture template yields the id it tests (…
claude Oct 12, 2026
3064551
fix(web): build i18n-check before running it in the image's i18n stage
claude Oct 12, 2026
f80f937
Merge remote-tracking branch 'origin/master' into claude/project-thre…
claude Oct 12, 2026
45c29e4
chore(i18n): go mod tidy
claude Oct 12, 2026
ad20922
fix(gp-module): check i18n command writes and stamp through os.Root
claude Oct 12, 2026
b1b2508
fix(operator): write condition ids through os.Root in the generator
claude Oct 12, 2026
4d69d0b
chore: merge master into claude/project-thread-awjqv0
claude Oct 12, 2026
45f060d
test: fix spec 019 test fixtures and lint findings
claude Oct 12, 2026
2ec9b39
test(i18n): use Template/Modèle in the module id check fixtures
claude Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ log "fetching envtest binaries for K8s 1.36.2"
# ---------- Go module cache warmup ----------
# Go modules from go.work. Derived from 'use' entries in go.work.
# Keep in sync with go.work when adding/removing modules.
for m in agent api audit-syslog-bridge capture-sidecar gameaction gameproto mcp-server netguard operator sentinel svcutil telemetry-receiver telemetryschema test/e2e tunnel; do
for m in agent api audit-syslog-bridge capture-sidecar gameaction gameproto i18n mcp-server netguard operator sentinel svcutil telemetry-receiver telemetryschema test/e2e tunnel; do
log "go mod download ($m)"
( cd "$m" && go mod download )
done
Expand Down
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ updates:
telemetryschema-minor-patch:
update-types: ["minor", "patch"]

- package-ecosystem: "gomod"
directory: "/i18n"
schedule:
interval: "weekly"
day: "monday"
time: "03:00"
commit-message:
prefix: "chore(deps)"
include: "scope"
open-pull-requests-limit: 5
groups:
i18n-minor-patch:
update-types: ["minor", "patch"]

- package-ecosystem: "gomod"
directory: "/telemetry-receiver"
schedule:
Expand Down
61 changes: 59 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ jobs:
- 'audit-syslog-bridge/**'
- 'telemetry-receiver/**'
- 'telemetryschema/**'
- 'i18n/**'
- 'sentinel/**'
- 'mcp-server/**'
- 'capture-sidecar/**'
Expand All @@ -136,7 +137,9 @@ jobs:
- '**/go.sum'
- '**/Dockerfile'
web:
# Language packs feed the dashboard build and the i18n checks in the web job.
- 'web/**'
- 'i18n/**'
charts:
- 'charts/**'
# The chart embeds a copy of the root signing key; the drift
Expand Down Expand Up @@ -604,7 +607,7 @@ jobs:
strategy:
fail-fast: false
matrix:
module: [netguard, gameaction, gameproto, telemetryschema, operator, agent, api, sentinel, audit-syslog-bridge, telemetry-receiver, mcp-server, capture-sidecar, svcutil, tunnel, gp-module, test/e2e]
module: [netguard, gameaction, gameproto, telemetryschema, i18n, operator, agent, api, sentinel, audit-syslog-bridge, telemetry-receiver, mcp-server, capture-sidecar, svcutil, tunnel, gp-module, test/e2e]
exclude: ${{ fromJSON(needs.changes.outputs.lint-exclude) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -683,7 +686,7 @@ jobs:
strategy:
fail-fast: false
matrix:
module: [netguard, gameaction, gameproto, telemetryschema, operator, api, agent, audit-syslog-bridge, telemetry-receiver, sentinel, mcp-server, capture-sidecar, svcutil, tunnel, gp-module]
module: [netguard, gameaction, gameproto, telemetryschema, i18n, operator, api, agent, audit-syslog-bridge, telemetry-receiver, sentinel, mcp-server, capture-sidecar, svcutil, tunnel, gp-module]
exclude: ${{ fromJSON(needs.changes.outputs.go-exclude) }}
arch: [amd64, arm64]
steps:
Expand Down Expand Up @@ -796,6 +799,60 @@ jobs:
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: { node-version: "24", cache: "npm", cache-dependency-path: web/package-lock.json }
- run: npm ci
- uses: ./.github/actions/go-cache
with:
key-suffix: web-i18n
- name: i18n language manifest
# The listing rule (80% complete, ltr, not en-XA) lives in the shared
# Go checker, so the manifest the dashboard build reads comes from it.
run: npm run i18n:manifest
- name: i18n checks
working-directory: .
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
set -euo pipefail
(cd web && npm run i18n:extract-only)
(cd api && go run ./cmd/messages-export > "$RUNNER_TEMP/registry.json")
base_flag=()
if [ -n "${BASE_SHA:-}" ] && [ "$BASE_SHA" != "0000000000000000000000000000000000000000" ]; then
if git fetch --no-tags --depth=1 origin "$BASE_SHA" 2>/dev/null \
&& git cat-file -e "$BASE_SHA:i18n/locales" 2>/dev/null; then
mkdir -p "$RUNNER_TEMP/base"
git archive "$BASE_SHA" i18n/locales | tar -x -C "$RUNNER_TEMP/base"
base_flag=(--base "$RUNNER_TEMP/base/i18n/locales")
fi
fi
cd i18n
go run ./cmd/i18n-check \
--extracted ../web/src/generated/i18n/extracted.json \
--registry "$RUNNER_TEMP/registry.json" --check-server \
"${base_flag[@]}" \
--summary "$GITHUB_STEP_SUMMARY" \
locales
- name: i18n-check self-test on seeded defective packs
# SC-007: every fixture must make the checker fail with exactly its
# own finding code (needs-review is a notice, so it passes).
working-directory: i18n
run: |
set -euo pipefail
go build -o "$RUNNER_TEMP/i18n-check" ./cmd/i18n-check
for dir in testdata/defective/*/; do
code=$(basename "$dir")
args=()
if [ -f "$dir/args" ]; then
read -r -a args < <(sed "s#{dir}#${dir%/}#g" "$dir/args")
fi
set +e
out=$("$RUNNER_TEMP/i18n-check" "${args[@]}" "${dir%/}")
status=$?
set -e
want=1; [ "$code" = needs-review ] && want=0
if [ "$status" -ne "$want" ] || ! grep -q "\[$code\]" <<<"$out"; then
echo "::error::fixture $code: exit $status (want $want), output:"; echo "$out"; exit 1
fi
echo "ok $code"
done
- name: eslint
run: npm run lint
- run: npm run build
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-edge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ on:
- "gameaction/**"
- "gameproto/**"
- "telemetryschema/**"
- "i18n/**"
- "svcutil/**"
- "gp-module/**"
- "audit-syslog-bridge/**"
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ web/coverage/
web/playwright-report/
web/test-results/
web/website-screenshots/
# Generated i18n artifacts (compiled catalogs, extracted messages, language manifest)
web/src/generated/
# Registry snapshot written by `make i18n-sync` and consumed by i18n-check
i18n/.registry.json
# Filtered language packs written by i18n-check --valid-out for the dashboard build
i18n/.valid/
web/e2e/.portforward.pid
*.log
.npmrc.local
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ reaches `1.0.0`. Pre-1.0 minor versions may contain breaking changes.
- **Nuclear Option player lists are structured:** the agent's player output
for JSON player lists now includes an entry per player (`steamId` and
`faction`), which the API uses for display-name lookup.
- **Language selection and translations (spec 019):** the dashboard can be
shown in another language. A user's language choice is saved to the account (or
to the browser when signed out), and an admin sets the instance default. Languages are
shared packs under `i18n/locales/`; a language is listed once its pack is at
least 80% translated, and anything missing falls back to English. French is
the first pack and is listed once it reaches 80%.
Dates, numbers and relative times follow the active language. A pseudo
language (`en-XA`) exists for maintainers behind `VITE_I18N_PSEUDO=1` and is
never listed in a release. See [`docs/translating.md`](docs/translating.md).
- **Translated server messages (opt-in `problem+json`):** API errors are sent
as `application/problem+json` with a stable `messageId` and arguments only
when the request lists that type in `Accept`; every other client keeps the
unchanged English plain-text body. The dashboard opts in and shows the
message in the active language. Game server condition messages carry
`status.conditionArgs` so the dashboard can translate them too; the English
`message` is unchanged.
- **Notification language per destination:** each notification destination
has an optional `language`; notifications and test sends to Discord, email
and webhooks are rendered in it, and destinations with none use the
instance default. Webhook payloads gain additive `language`, `title` and
`text` fields; existing fields are unchanged.
- **Module translations:** a module can ship `locales/<tag>.json` next to
`module.yaml` (OCI layer type `application/vnd.gameplane.module.locale.v1+json`
and the same files in every other source). The operator validates them on
install, stores the valid entries in a `<template>-locales` ConfigMap and
reports problems as the `LocalesValid` condition; install never fails
because of translations. Each file is read up to 900 KiB and all of a
bundle's files together up to 4 MiB. `gp-module i18n extract|stamp` and
`gp-module validate` support authoring. See
[`docs/module-authoring.md`](docs/module-authoring.md).

### Changed

Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ netguard/ SSRF dial-guard: IsAllowed (operator module sources, API si
gameaction/ console-injection guard + command renderer (api, agent)
gameproto/ Minecraft/Terraria handshake parser (sentinel)
telemetryschema/ shared telemetry report contract (api, telemetry-receiver)
i18n/ shared language packs, ICU formatter, pack validator, i18n-check (api, web, operator, gp-module)
gp-module/ module authoring CLI: init/validate/preview/package
operator/ controller-runtime operator
api/v1alpha1/ CRD types (edit here, then `make generate manifests`); zz_generated.deepcopy.go GENERATED
Expand All @@ -60,7 +61,7 @@ test/e2e/ Kind E2E suite (//go:build e2e)
docs/ architecture, security, modules
design.pen canonical Pencil dashboard design
signing/ cosign public keys, dated legacy key + cross-signature (image + module)
go.work links all 16 Go modules (incl. test/e2e)
go.work links all 17 Go modules (incl. test/e2e)
Makefile canonical task runner
```

Expand All @@ -75,7 +76,7 @@ After cloning: `git submodule update --init` (`modules/` required for `make dev-
- **E2E conventions:** register new tests in `test/e2e/buckets.sh`; `t.Parallel()` + unique names; guard shared resources (`ociPushMu` for module pushes, `ensureResticRepo(t)` for shared backup repos). Rate limits per cluster: IP burst 10 (5/min), user burst 6 (3/min) — cap an API bucket at ~7 admin logins.
- **Lint:** `make lint` = gofmt, go vet, golangci-lint, ESLint, `check-specs`, `check-doc-versions`, `check-links`.

**Coverage minimums:** netguard 91, gameaction 91, gameproto 90, telemetryschema 90, gp-module 80, operator 72, api 80, agent 90, svcutil 90; audit-syslog-bridge, telemetry-receiver, sentinel, capture-sidecar, mcp-server, tunnel 70; web 92 lines / 76 functions / 82 branches / 92 statements.
**Coverage minimums:** netguard 91, gameaction 91, gameproto 90, telemetryschema 90, i18n 90, gp-module 80, operator 72, api 80, agent 90, svcutil 90; audit-syslog-bridge, telemetry-receiver, sentinel, capture-sidecar, mcp-server, tunnel 70; web 92 lines / 76 functions / 82 branches / 92 statements.

## Core rules

Expand Down Expand Up @@ -116,6 +117,7 @@ After cloning: `git submodule update --init` (`modules/` required for `make dev-
| gameaction | Go | validates console input against schemas; escapes injection |
| gameproto | Go | Minecraft/Terraria wire parser for connection filtering |
| telemetryschema | Go | stdlib-only shared telemetry report contract: types, strict decode, official module catalog, report signing (api, telemetry-receiver) |
| i18n | Go | shared language packs (`go:embed`), ICU MessageFormat subset, pack validator, `i18n-check` CI tool (api, web, operator, gp-module) |
| gp-module | Go | module CLI: scaffold, offline validate, dry-run preview, OCI package |
| svcutil | Go | stdlib-only env + graceful shutdown (`RunHTTP`) |
| operator | Go, controller-runtime | authoritative reconciler for 9 CRDs (`GameServer`, `GameTemplate`, …) |
Expand Down
18 changes: 16 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ KUBECONFIG_ENV :=
KUBE_CONTEXT := kind-$(KIND_CLUSTER)
endif

GO_MODULES := netguard gameaction gameproto telemetryschema gp-module operator api agent audit-syslog-bridge telemetry-receiver sentinel mcp-server capture-sidecar svcutil tunnel
GO_MODULES := netguard gameaction gameproto telemetryschema i18n gp-module operator api agent audit-syslog-bridge telemetry-receiver sentinel mcp-server capture-sidecar svcutil tunnel
GO_INTEGRATION_MODULES := operator api

# test/e2e is a separate module (its own go.mod, excluded from the coverage
Expand Down Expand Up @@ -259,6 +259,20 @@ check-links: ## Verify internal documentation links and anchors resolve
check-telemetry-catalog: ## Verify telemetryschema/catalog.txt matches the chart's official module list
hack/check-telemetry-catalog.sh

.PHONY: i18n-sync
i18n-sync: ## Write the API message registry (server.* and notify.*) and the operator condition.* ids into the English reference (i18n/locales/en/app.json)
cd api && go run ./cmd/messages-export > ../i18n/.registry.json
cd i18n && go run ./cmd/i18n-check --registry .registry.json --sync-server locales
rm -f i18n/.registry.json
cd operator && go run ./hack/condition-ids -app ../i18n/locales/en/app.json

.PHONY: check-i18n
check-i18n: ## Verify language packs, server.*, notify.* and operator condition.* drift and dashboard extraction drift (spec 019)
cd web && npm run i18n:extract-only
cd operator && go run ./hack/condition-ids -check -app ../i18n/locales/en/app.json
cd api && go run ./cmd/messages-export > ../i18n/.registry.json
cd i18n && status=0; go run ./cmd/i18n-check --extracted ../web/src/generated/i18n/extracted.json --registry .registry.json --check-server locales || status=$$?; rm -f .registry.json; exit $$status

.PHONY: check-dev-load-images
check-dev-load-images: ## Verify `dev-load` loads every image `images` builds
hack/check-dev-load-images.sh
Expand All @@ -273,7 +287,7 @@ test-up-registry: ## Run the fixture tests for deploy/kind/up.sh's registry/cont
hack/test-up-registry.sh

.PHONY: lint
lint: check-doc-versions check-links check-specs check-telemetry-catalog check-dev-load-images check-coverage-gaps test-up-registry lint-go lint-web ## Run all linters
lint: check-doc-versions check-links check-specs check-telemetry-catalog check-i18n check-dev-load-images check-coverage-gaps test-up-registry lint-go lint-web ## Run all linters

.PHONY: lint-go
lint-go: ## Run golangci-lint across all modules
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ Design changes go through `design.pen` (Pencil) before any UI code is
written. See [`docs/contributing.md`](docs/contributing.md) for the
full guide: code style, test tiers, and the PR process.

Translations are welcome. See [`docs/translating.md`](docs/translating.md)
to add or improve a language pack.

## License

[AGPL-3.0-or-later](./LICENSE). Any network-accessible deployment of a
Expand Down
5 changes: 3 additions & 2 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS build
WORKDIR /src

# netguard, gameaction, gp-module, and telemetryschema are in-repo modules the API depends
# on via local replaces (../netguard, ../gameaction, ../gp-module, ../telemetryschema);
# netguard, gameaction, gp-module, telemetryschema, and i18n are in-repo modules the API depends
# on via local replaces (../netguard, ../gameaction, ../gp-module, ../telemetryschema, ../i18n);
# copy them first so `go mod download` and the build resolve them.
COPY netguard/ ./netguard/
COPY gameaction/ ./gameaction/
COPY gp-module/ ./gp-module/
COPY telemetryschema/ ./telemetryschema/
COPY i18n/ ./i18n/
COPY api/go.mod api/go.sum* ./api/
WORKDIR /src/api
RUN go mod download
Expand Down
4 changes: 4 additions & 0 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@ func main() {
// registry per request so config saves apply without a restart.
// No version/host/count — login-privacy.
r.Get("/providers", handlers.AuthProvidersHandler(authRegistry))
// Pre-auth: the instance default language for the login page. Only the
// tag is returned, never more (login privacy); not rate-limited, like
// /providers.
r.Get("/locale", handlers.AuthLocaleHandler(store))
// Rate-limit /login specifically — argon2id is ~200 ms of
// single-core CPU per attempt, so an unlimited path invites a
// trivial DoS.
Expand Down
19 changes: 19 additions & 0 deletions api/cmd/messages-export/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Command messages-export writes the API message registry as JSON to stdout,
// together with the notify.* notification text. The i18n-sync Makefile target
// feeds the output to i18n-check.
package main

import (
"fmt"
"os"

"github.com/GameplanePanel/gameplane/api/internal/messages"
"github.com/GameplanePanel/gameplane/api/internal/notify"
)

func main() {
if err := messages.ExportJSON(os.Stdout, notify.Messages()...); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
5 changes: 5 additions & 0 deletions api/docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ paths:
responses:
'200':
description: OK
'/modules/{name}/locales/{tag}':
get:
responses:
'200':
description: OK
'/namespaces':
get:
responses:
Expand Down
7 changes: 6 additions & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ require github.com/GameplanePanel/gameplane/telemetryschema v0.0.0
// `go build ./api/...`.
replace github.com/GameplanePanel/gameplane/telemetryschema => ../telemetryschema

// i18n is an in-repo module (no published version); resolve it locally
// both inside the go.work workspace and for standalone `go build ./api/...`.
replace github.com/GameplanePanel/gameplane/i18n => ../i18n

require (
github.com/GameplanePanel/gameplane/i18n v0.0.0
github.com/coder/websocket v1.8.15
github.com/coreos/go-oidc/v3 v3.21.0
github.com/go-chi/chi/v5 v5.3.2
Expand All @@ -43,6 +48,7 @@ require (
golang.org/x/oauth2 v0.37.0
golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
golang.org/x/text v0.42.0
k8s.io/api v0.37.1
k8s.io/apimachinery v0.37.1
k8s.io/client-go v0.37.1
Expand Down Expand Up @@ -109,7 +115,6 @@ require (
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
Expand Down
Loading
Loading