i18n key conventions
This page documents the namespace rules every CRM-side t(...) call
must follow. They exist so the locale files stay greppable and three
parallel translation files don't drift over time.
The three rules
1. Module → entity → field/section
{module}.{entity}.{section}.{key}
Examples:
crm.contacts.fields.firstName— the form-field label.crm.contacts.placeholders.firstName— the input placeholder.crm.contacts.notifications.created— the success snackbar.crm.contacts.deleteConfirmTitle— top-level keys are allowed when the value is page-global (no obvious section).
2. Detail pages live under <entity>Detail, not <entity>.detail
This is a legacy convention. The contact detail page reads from
crm.contactDetail.*, NOT crm.contacts.detail.*. CRM audit F0 #23
flagged the hierarchy mismatch; we keep the legacy structure rather
than rename 56 keys × 3 locales mid-refactor. New detail pages
SHOULD continue the pattern ({entity}Detail) for consistency with
the existing ones.
| Page | Namespace |
|---|---|
crm/contacts/[id].vue | crm.contactDetail.* |
crm/companies/[id].vue | crm.companyDetail.* |
| (future) | crm.{entity}Detail.* |
3. Notifications follow a strict 6-key shape
Every CRUD store dispatches one of:
{namespace}.notifications.created
{namespace}.notifications.createFailed
{namespace}.notifications.updated
{namespace}.notifications.updateFailed
{namespace}.notifications.deleted
{namespace}.notifications.deleteFailed
Domain-specific verbs (publish, archive, retry) get one pair each:
{namespace}.notifications.published / publishFailed
{namespace}.notifications.unpublished / unpublishFailed
{namespace}.notifications.archived / archiveFailed
Use useApiError(errorRef).notifySuccess(key) /
notifyError(err, fallbackKey) so the i18n key always falls back to
the server-provided message field, then the i18n key, then the
fallback.
Adding a new locale string
- Write the source-language entry in
hu.json(the editorial source of truth — translators read this). - Mirror the same key + structure in
en.jsonandde.json. - If the key namespace doesn't exist yet, place it in the same parent block where related keys live (don't create a new top-level namespace unless you really need one).
- Run
npm run test— the locale-parity test fails if you add a key to one file but forget the other two.
Common mistakes
- Don't write
t('Some hardcoded English'). The argument must be a dotted key path, NOT the literal text. - Don't introduce
t('${someVar}.foo')— i18n keys must be static so the linter can detect missing translations. Uset('crm.statuses.' + status)only when the value side of the branch is short and the keys are explicitly enumerated. - Don't put placeholder values directly in the template
(
placeholder="Acme Inc."). Always go throught('...placeholder')— seecrm.companies.placeholders.namefor the canonical example. - Don't mix
crm.contacts.titleandcrm.contacts.headerTitleto mean the same thing — pick one and use it everywhere.