Skip to content

Foundation — i18n keys

Scope note: After the M0 cleanup ticket, foundation owns only two always-loaded namespaces: common and validation. The identity namespace moves to lazy-loaded — it has no content today (the vibe-coded login is being deleted) and will be re-introduced (along with its always-loaded designation, if needed) by identity's first ticket. Per-module namespaces (item-catalog, manufacturing, …) are created by each module's first ticket and live outside this plan.

The validation namespace is never written manuallyzod-i18n-map provides the keys. The list below is the structural reference for what should exist; if anything's missing from the actual JSON files after foundation tickets land, that's a bug fix in the M0 cleanup ticket.

Locale files: src/locales/en/{common,validation}.json and src/locales/pl/{common,validation}.json. The identity.json files are emptied (or deleted) by the M0 cleanup ticket — left empty {} if their existence is convenient for tooling, deleted otherwise.

Namespace: common

Loaded always. Used by every page.

Key en pl
nav.dashboard Dashboard Pulpit

Module-grouping section headers (nav.group.identity, nav.group.itemCatalog, etc.) are added by each module's first ticket — foundation seeds none of them since no module has shipped yet.

actions.* — semantic button labels

Owned by SaveButton, CancelButton, DeleteButton, etc. Call sites never pass these strings.

Key en pl
actions.save Save Zapisz
actions.cancel Cancel Anuluj
actions.delete Delete Usuń
actions.confirm Confirm Potwierdź
actions.edit Edit Edytuj
actions.add Add Dodaj
actions.apply Apply Zastosuj
actions.reset Reset Resetuj
actions.refresh Refresh Odśwież
actions.backToDashboard Back to dashboard Wróć do panelu

state.* — page/data states

Key en pl
state.loading Loading… Ładowanie…
state.empty No data to display Brak danych
state.refreshing Refreshing… Odświeżanie…
state.saving Saving… Zapisywanie…

errors.* — error messages used app-wide

Two key styles coexist here. Codes emitted by the BE (grinsystem-api/src/shared/exceptions.py) use the literal BE code as the i18n key — UPPER_SNAKE — so the lookup is a mechanical concat (common:errors.${err.code}) with no transform layer that could drift. Transport / general / page-chrome strings use the lowerCamelCase convention everywhere else in this doc. This is the one explicit exception to convention §1 below.

The full keyset is landed by GRF-9 (M3 error envelope foundation).

BE-code keys (one per shared exception class)

Key en pl
errors.VALIDATION_ERROR Some of the entered data is invalid. Niektóre dane są nieprawidłowe.
errors.CONCURRENT_MODIFICATION This record was changed by someone else. Refresh to see the latest version. Ten rekord został zmieniony przez kogoś innego. Odśwież, aby zobaczyć aktualną wersję.
errors.FORBIDDEN_INSUFFICIENT_PERMISSION You don't have permission for this action. Brak uprawnień do tej akcji.
errors.FORBIDDEN Access denied. Brak dostępu.
errors.UNAUTHORIZED Your session has expired. Sign in again. Sesja wygasła. Zaloguj się ponownie.
errors.NOT_FOUND The requested resource was not found. Nie znaleziono żądanego zasobu.
errors.CONFLICT Operation conflicts with the current state. Operacja jest sprzeczna z aktualnym stanem.
errors.IDEMPOTENCY_KEY_REQUIRED Internal error: missing idempotency key. Try again. Błąd wewnętrzny: brak klucza idempotentności. Spróbuj ponownie.
errors.IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD This action was already submitted with a different payload. Ta akcja została już wysłana z innymi danymi.
errors.IMMUTABLE_FIELD_CANNOT_BE_CHANGED Internal error: tried to change a read-only field. Błąd wewnętrzny: próba zmiany pola tylko do odczytu.
errors.INVALID_SORT_FIELD Invalid sort option. Resetting to default. Nieprawidłowa opcja sortowania. Przywrócono domyślną.
errors.REPOSITORY_ERROR Server error. Try again later. Błąd serwera. Spróbuj ponownie później.
errors.DOMAIN_ERROR Operation could not be completed. Nie udało się wykonać operacji.

Transport / page-chrome keys (lowerCamelCase)

Key en pl
errors.networkError Could not reach the server. Check your connection and try again. Nie udało się połączyć z serwerem. Sprawdź połączenie i spróbuj ponownie.
errors.serverError Something went wrong. Try again later. Coś poszło nie tak. Spróbuj ponownie później.
errors.notFound Not found Nie znaleziono
errors.unexpected An unexpected error occurred. Please try again. Wystąpił nieoczekiwany błąd. Spróbuj ponownie.
errors.insufficientPermission You don't have permission for this action. Brak uprawnień do tej akcji.
errors.forbiddenTitle Access denied Brak dostępu
errors.forbiddenBody Contact your administrator if you believe you should have access to this page. Skontaktuj się z administratorem, jeśli uważasz, że ta strona powinna być dostępna.

errors.insufficientPermission is the i18n key referenced by the Permission-Gated UI convention — see ../../standards.md § Permission-Gated UI. Per-module <module>:errors.<CODE> entries are added opportunistically by each feature module's first form that can trigger them — see data-fetching.md § Errors.

confirm.* — generic confirmation dialog text

Key en pl
confirm.deleteTitle Delete this item? Usunąć ten element?
confirm.deleteBody This action cannot be undone. Tej akcji nie można cofnąć.
confirm.discardChangesTitle Discard changes? Odrzucić zmiany?
confirm.discardChangesBody Your unsaved changes will be lost. Niezapisane zmiany zostaną utracone.

app.* — app-level branding

Key en pl
app.name GrinSystem GrinSystem

User-menu items (app.userMenu.account, app.userMenu.signOut) land with identity's first ticket — they have nothing to point to until then.


Namespace: identity

Empty in foundation. The pre-existing identity.json files (with vibe-coded login keys) are emptied or deleted by the M0 cleanup ticket. Identity's first ticket re-seeds this namespace with nav.login, login.*, account.*, permission.*, etc. — and decides at that point whether it warrants always-loaded status (probably yes, since the login screen has to render before any auth).


Namespace: validation

Never authored by hand. This namespace is supplied by zod-i18n-map. The package ships translations for every built-in Zod issue type; we only override an entry if the default Polish is awkward in a domain context.

Current overrides: none. If a module needs a custom message (e.g. "SKU must match pattern XYZ"), the message is supplied at schema definition time via .message('keyName') and resolved against the module's own namespace, not validation. Keep validation strictly for Zod's built-ins.


Conventions

  1. Keys are dot-notation paths, lowerCamelCase segments. Never kebab-case in keys. Exception: errors.<BE_CODE> entries use the literal UPPER_SNAKE BE exception code as the segment so the common:errors.${err.code} lookup is mechanical — see § errors.* above.
  2. One JSON file per namespace per localesrc/locales/{en,pl}/{namespace}.json. No nesting JSON files inside subdirectories.
  3. Status enums translate by value — example: t(\itemType.${item.item_type}`). The mapping lives in the module namespace, not incommon`.
  4. Polish is the primary locale. Write the Polish copy first when drafting new keys; the English is a translation, not the source.
  5. Semantic buttons own their labels. Call sites never pass text. <SaveButton /> resolves common:actions.save internally — passing a label prop is forbidden.
  6. Placeholders use {{name}} syntax per i18next convention. Example: t('itemCount', { count: 5 })"5 items" / "5 elementów" (with _one / _other plural variants in the JSON).