feat(issue-226): add shared backend-frontend key-map and locale contract
This commit is contained in:
30
docs/ISSUE-226-SHARED-KEYMAP-LOCALE-CONTRACT.md
Normal file
30
docs/ISSUE-226-SHARED-KEYMAP-LOCALE-CONTRACT.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# ISSUE-226 — Shared key-map + locale-kontrakt (backend/frontend)
|
||||
|
||||
## Source of truth
|
||||
- Single shared artifact: `shared/i18n/lobby.json`
|
||||
- Ownership is documented under `contract.ownership` in the same artifact.
|
||||
|
||||
## Locale contract
|
||||
Defined under `contract.locale`:
|
||||
- default locale: `en`
|
||||
- supported locales: `en`, `da`
|
||||
- fallback rule: use default locale when requested locale is unsupported or a key translation is missing.
|
||||
|
||||
## Shared backend→frontend key-map
|
||||
Defined under `contract.backend_to_frontend_error_keys`.
|
||||
|
||||
Examples:
|
||||
- `session_not_found -> session_not_found`
|
||||
- `session_not_joinable -> join_failed`
|
||||
- `round_start_invalid_phase -> start_round_failed`
|
||||
|
||||
This allows backend error codes to remain stable while frontend copy keys stay UX-oriented.
|
||||
|
||||
## da/en example values
|
||||
From `shared/i18n/lobby.json`:
|
||||
- `frontend.errors.session_code_required.en = "Session code is required."`
|
||||
- `frontend.errors.session_code_required.da = "Sessionskoden er påkrævet."`
|
||||
|
||||
## Verification
|
||||
- Backend: `python manage.py test lobby.tests.I18nResolverTests`
|
||||
- Frontend: `npm test -- --run tests/lobby-i18n.contract.test.ts`
|
||||
@@ -14,10 +14,16 @@ describe('shared i18n keyspace contract', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps backend error-code keyspace aligned with backend translations', () => {
|
||||
for (const [code, key] of Object.entries(lobbyCatalog.backend.error_codes)) {
|
||||
expect(code).toBe(key);
|
||||
expect(lobbyCatalog.backend.errors[key as keyof typeof lobbyCatalog.backend.errors]).toBeDefined();
|
||||
it('keeps backend error-code keyspace aligned with shared backend→frontend map and backend translations', () => {
|
||||
for (const [code, backendKey] of Object.entries(lobbyCatalog.backend.error_codes)) {
|
||||
const frontendKey =
|
||||
lobbyCatalog.contract.backend_to_frontend_error_keys[
|
||||
code as keyof typeof lobbyCatalog.contract.backend_to_frontend_error_keys
|
||||
];
|
||||
|
||||
expect(lobbyCatalog.backend.errors[backendKey as keyof typeof lobbyCatalog.backend.errors]).toBeDefined();
|
||||
expect(frontendKey, `missing frontend mapping for ${code}`).toBeTruthy();
|
||||
expect(lobbyCatalog.frontend.errors[frontendKey as keyof typeof lobbyCatalog.frontend.errors]).toBeDefined();
|
||||
}
|
||||
|
||||
for (const [key, translations] of Object.entries(lobbyCatalog.backend.errors)) {
|
||||
@@ -42,7 +48,7 @@ describe('lobbyMessage locale handling', () => {
|
||||
).toBe('Sessionskoden er ugyldig, eller sessionen findes ikke længere.');
|
||||
});
|
||||
|
||||
it('falls back when backend error code has no frontend translation key', () => {
|
||||
it('uses shared backend→frontend key-map when backend key differs from frontend copy key', () => {
|
||||
expect(
|
||||
lobbyMessageFromApiPayload(
|
||||
{ error_code: 'session_not_joinable', locale: 'da' },
|
||||
@@ -50,4 +56,13 @@ describe('lobbyMessage locale handling', () => {
|
||||
),
|
||||
).toBe('Kunne ikke joine. Tjek kode eller kaldenavn og prøv igen.');
|
||||
});
|
||||
|
||||
it('falls back to caller-provided fallback key for unknown backend error codes', () => {
|
||||
expect(
|
||||
lobbyMessageFromApiPayload(
|
||||
{ error_code: 'unknown_backend_key', locale: 'da' },
|
||||
'join_failed',
|
||||
),
|
||||
).toBe('Kunne ikke joine. Tjek kode eller kaldenavn og prøv igen.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1279,7 +1279,8 @@ class I18nResolverTests(TestCase):
|
||||
frontend_errors = catalog["frontend"]["errors"]
|
||||
shared_map = catalog["contract"]["backend_to_frontend_error_keys"]
|
||||
|
||||
for code, key in catalog["backend"]["error_codes"].items():
|
||||
self.assertEqual(shared_map.get(code), key)
|
||||
self.assertIn(key, backend_errors)
|
||||
self.assertIn(key, frontend_errors)
|
||||
for code, backend_key in catalog["backend"]["error_codes"].items():
|
||||
frontend_key = shared_map.get(code)
|
||||
self.assertIn(backend_key, backend_errors)
|
||||
self.assertTrue(frontend_key, f"missing frontend mapping for backend code: {code}")
|
||||
self.assertIn(frontend_key, frontend_errors)
|
||||
|
||||
@@ -321,5 +321,31 @@
|
||||
"da": "Runden er allerede konfigureret"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract": {
|
||||
"ownership": {
|
||||
"artifact": "shared/i18n/lobby.json",
|
||||
"backend": "lobby/* reads backend/errors + backend/error_codes",
|
||||
"frontend": "frontend/* reads frontend/errors + frontend/ui + contract/backend_to_frontend_error_keys"
|
||||
},
|
||||
"locale": {
|
||||
"default": "en",
|
||||
"supported": [
|
||||
"en",
|
||||
"da"
|
||||
],
|
||||
"fallback": "Use default locale when requested locale is unsupported or key translation is missing."
|
||||
},
|
||||
"backend_to_frontend_error_keys": {
|
||||
"session_code_required": "session_code_required",
|
||||
"nickname_invalid": "nickname_invalid",
|
||||
"session_not_found": "session_not_found",
|
||||
"session_not_joinable": "join_failed",
|
||||
"nickname_taken": "nickname_taken",
|
||||
"category_slug_required": "start_round_failed",
|
||||
"category_not_found": "start_round_failed",
|
||||
"round_start_invalid_phase": "start_round_failed",
|
||||
"round_already_configured": "start_round_failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user