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', () => {
|
it('keeps backend error-code keyspace aligned with shared backend→frontend map and backend translations', () => {
|
||||||
for (const [code, key] of Object.entries(lobbyCatalog.backend.error_codes)) {
|
for (const [code, backendKey] of Object.entries(lobbyCatalog.backend.error_codes)) {
|
||||||
expect(code).toBe(key);
|
const frontendKey =
|
||||||
expect(lobbyCatalog.backend.errors[key as keyof typeof lobbyCatalog.backend.errors]).toBeDefined();
|
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)) {
|
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.');
|
).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(
|
expect(
|
||||||
lobbyMessageFromApiPayload(
|
lobbyMessageFromApiPayload(
|
||||||
{ error_code: 'session_not_joinable', locale: 'da' },
|
{ 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.');
|
).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"]
|
frontend_errors = catalog["frontend"]["errors"]
|
||||||
shared_map = catalog["contract"]["backend_to_frontend_error_keys"]
|
shared_map = catalog["contract"]["backend_to_frontend_error_keys"]
|
||||||
|
|
||||||
for code, key in catalog["backend"]["error_codes"].items():
|
for code, backend_key in catalog["backend"]["error_codes"].items():
|
||||||
self.assertEqual(shared_map.get(code), key)
|
frontend_key = shared_map.get(code)
|
||||||
self.assertIn(key, backend_errors)
|
self.assertIn(backend_key, backend_errors)
|
||||||
self.assertIn(key, frontend_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"
|
"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