Installs into .claude/skills of the current project.
Are you the author of Conventions I18n?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/stella-conventions-i18n)
---
name: conventions-i18n
description: "Internationalization conventions for Stella. Apply when adding or modifying user-facing strings."
---
# i18n Conventions
Internationalization conventions for Stella. Apply when adding or
modifying user-facing strings.
## Stack
`use-intl` for runtime.
## Supported Languages
en is the source language. Check `apps/web/src/i18n/langs/` for
the current list of target languages (add translations to every
`.json` file found there).
## Translation Flow
1. Add or modify keys in
`apps/web/src/i18n/langs/en.json`.
2. Add corresponding translations to **all target language files**
found in `apps/web/src/i18n/langs/` (every `.json` file
except `en.json`). Write natural, idiomatic translations;
avoid literal/robotic phrasing.
3. Run `bun run i18n:sync` from `apps/web`. This synchronizes locale
structure, regenerates typed messages, and updates generated glossary
output. Never edit `messages.gen.ts` or generated terminology tables by
hand.
4. Run `bun run i18n:check` from `apps/web`. Typecheck does not regenerate or
validate the catalogs, so a clean typecheck is not evidence that i18n is in
sync.
5. Read the rendered sentence in context, including interpolation and plural
branches. Passing key parity is not proof that a translation is natural or
that placeholders remain grammatically valid.
## Key Naming
**Prefer generic, reusable keys over feature-specific ones.**
Before adding any new i18n key, search `en.json` for an existing
key with the same or similar wording (e.g., `common.filter`,
`common.sort`, `common.columns`). Reuse `common.*` or shared
namespace keys instead of creating feature-scoped duplicates
like `billing.expenses.deleteExpense`. Feature-specific keys
are only justified when the wording truly differs from the
generic version (e.g., a confirmation message that mentions
the resource by name).
When a translation key crosses indirection through a constant, map, prop, or
helper return type, type it with `TranslationKey` from
`apps/web/src/i18n/types.ts` instead of `string`. Missing or stale keys must
fail typecheck at the point where they are stored.
Prefer complete translatable sentences over fragments assembled in JSX. Keep
interpolation variables semantic (`{documentName}`, not `{value}`), use ICU
plural/select branches for grammatical variation, and never concatenate
translated fragments whose word order differs by locale.
**Never call anything an "entity" in user-facing copy**, in English or as a
calque (`entita`, `Entität`, `entidad`, `entité`, `entidade`, `encja`, ...).
It is the database's word for a row and means nothing to a lawyer. Name the
concrete thing the string is about — document, file, folder, task, matter —
and fall back to "item" only when the string genuinely covers all of them. In
anonymization, what the detector finds is a **match**, not an entity. The
`item` concept in `glossary.json` enforces this with `forbiddenAlways`, so it
fires on the English source too, not only on translations.
Key naming, pluralization, and style rules are documented
in `apps/web/src/i18n/TERMINOLOGY.md`.
## Terminology (glossary)
Canonical legal/domain terms live in
`apps/web/src/i18n/glossary.json` (the source of truth);
`glossary-gen` renders them into the tables in `TERMINOLOGY.md`.
The `i18n-lint` checker enforces them: per concept it flags a
translation that uses a `forbidden` rendering when the English
source is about that concept (concept-gated, so a common word only
fires in the right context).
When introducing a NEW concept (or changing a preferred term):
1. **Research it first.** Confirm the sector-standard term and the
synonyms to avoid in **each** language against authoritative
sources (IATE/EU terminology, national legal glossaries,
established legal-tech usage) — do not guess. Record the
rationale in the concept's `note`.
2. **Add it to `glossary.json`**, never only to `TERMINOLOGY.md`
(the `.md` is generated). Run `bun run i18n:sync` (from
`apps/web`) to regenerate the tables.
3. **Account for declensions/inflections.** The lint matches
forbidden terms whole-word (English source triggers also match
their regular plural), so list the actual inflected, declined,
and compound forms a translator might use — e.g. de
`Sache`/`Sachen`/`Mandatsdaten`, sk `Vec`/`veci`/`vecou`, et
`asi`/`asja`/`asjad`. A base form alone misses inflected drift.
4. **Pick the right ban field.** `forbidden` is concept-gated: it fires
only where the English source names the concept (or a `keyTriggers`
path matches), which is what stops a common word from false-firing
everywhere. `forbiddenAlways` drops that gate, for wording that is
wrong in every context. Use it when a concept-gated ban would go
blind the moment someone rewords the English source, and only once
you have checked that no legitimate use of the word exists in that
locale.
5. **Apply consistently.** `bun run i18n:check` fails on new
forbidden renderings; fix the translations, or — only for
genuine pre-existing debt — grandfather with
`i18n-lint <dir> --write-baseline` and flag for native review.
## Landing (marketing) catalogs
The landing app has its own catalogs (`apps/landing/src/i18n/messages/`)
with the same glossary enforcement plus marketing-specific rules —
register per locale, the identity phrase, hero structure differences,
meta-string length budgets, cognate policy, and brand handling. Before
translating landing strings, read
`apps/landing/src/i18n/TRANSLATION.md`; it explains the intent behind
each rule so translations stay coherent rather than merely passing the
checks. Landing gates: `bun run i18n:sync` and `bun run i18n:check`
from `apps/landing`.