# General Translation Knowledge Base: How should I name i18n translation keys?
URL: https://generaltranslation.com/en-GB/kb/i18n-key-naming.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Name keys by where text appears and what it means, such as checkout.summary.title, not by its English wording. Give each distinct use its own key, even when the English matches: German translates the button Open as Öffnen and the status Open as Offen.
Category: Internationalisation
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

A key is the identifier your code uses to look up a translation, as in `t('summary.title', { ns: 'checkout' })`. Good names survive copy edits, tell translators where the text appears, and make dead keys easy to find. The conventions below apply to i18next, react-i18next, Rails I18n, vue-i18n, and any other library that reads keyed JSON or YAML; the examples use i18next syntax, run with i18next 26.5.3.

## 1. Namespace by feature or screen

Start every key with the area of the product it belongs to, then the component, then the element.

* ✅ `checkout.summary.title`, `settings.billing.cancelButton`
* ❌ `title1`, `text_47`, `newString`

When the first segment is a separate file, it&#39;s a namespace rather than part of the key path. With `summary.title` stored in `checkout.json`, i18next finds it as `t('summary.title', { ns: 'checkout' })` or `t('checkout:summary.title')`; `t('checkout.summary.title')` returns the key string itself, because it looks for a `checkout` object in the default namespace.

The prefix tells a translator where the text appears and lets you split catalogues into files. i18next calls these files [namespaces](https://www.i18next.com/principles/namespaces) and suggests splitting per view, per feature, or per lazy-loaded module, plus a `common` namespace for labels such as Save and Cancel. In React, react-i18next loads one with `useTranslation('checkout')`, and the [`keyPrefix` option](https://react.i18next.com/latest/usetranslation-hook) shortens deep paths.

## 2. Describe the meaning, not the wording

A key should still be right after marketing rewrites the sentence.

* ✅ `auth.login.submit`
* ❌ `auth.login.clickHereToSignInNow`

When the button later says &#39;Continue&#39;, the first key is still accurate. The second now describes text that no longer exists, and someone will eventually rename it, which breaks every reference and orphans the translations.

## 3. One key per context, not one per English string

Two strings that read the same in English often translate differently. &#39;Open&#39; on a button is a verb; &#39;Open&#39; as a ticket status is an adjective. German uses *Öffnen* for the first and *Offen* for the second.

* ✅ `files.actions.open` and `tickets.status.open`
* ❌ one `common.open` reused for both

Reserve the shared `common` namespace for labels whose meaning is identical everywhere, such as a Cancel button. Source-text systems solve the same problem with context: GNU gettext&#39;s [`msgctxt`](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html) lets one English string carry two translations.

## 4. Pick nested or flat JSON, and don&#39;t mix them

Inside `checkout.json`, i18next reads `summary.title` as a path, because its `keySeparator` defaults to `.`. Both shapes work; pick one so every file, reviewer, and tool agrees.

```json title="locales/en/checkout.json (nested)"
{
  "summary": {
    "title": "Order summary",
    "taxNotice": "Tax is calculated at checkout."
  }
}
```

```json title="locales/en/checkout.json (flat)"
{
  "summary.title": "Order summary",
  "summary.taxNotice": "Tax is calculated at checkout."
}
```

For flat files, the i18next [configuration docs](https://www.i18next.com/overview/configuration-options) recommend setting `keySeparator` to `false`. Choose a casing too (`camelCase` or `snake_case` segments) and use it in every file.

## 5. Use your library&#39;s plural and context suffixes

i18next selects plural forms by suffix and requires the variable to be called `count`. A `_zero` entry overrides the language&#39;s rule when the count is 0, and ordinals add `_ordinal`:

```json title="locales/en/checkout.json"
{
  "cart": {
    "itemCount_zero": "Your cart is empty",
    "itemCount_one": "{{count}} item",
    "itemCount_other": "{{count}} items"
  }
}
```

`t('cart.itemCount', { count: 0 })` returns &quot;Your cart is empty&quot;, 1 returns &quot;1 item&quot;, and 5 returns &quot;5 items&quot;. A language with more plural categories adds keys such as `itemCount_few` and `itemCount_many`. (See the [i18next plurals docs](https://www.i18next.com/translation-function/plurals), and [What is ICU message format?](/kb/icu-message-format) for each language&#39;s categories.)

* ✅ `itemCount_one`, `itemCount_other`, and for gendered text, [context](https://www.i18next.com/translation-function/context) keys like `friend_male` and `friend_female`
* ❌ invented suffixes such as `itemCount_singular` and `itemCount_plural`, which the library won&#39;t select

Libraries built on ICU message format (react-intl, next-intl, Lingui) keep every plural form inside one message under one key instead.

## 6. Never build a sentence from several keys

Word order changes between languages, so a sentence split across keys can&#39;t be translated correctly.

* ❌ `t('account.hello') + name + t('account.welcomeBack')`
* ✅ one key with a placeholder: `"account.welcome": "Hello, {{name}}! Welcome back."`

When a message includes a count, keep the whole sentence in each plural form too, using the suffixes from convention 5.

The [GNU gettext manual](https://www.gnu.org/software/gettext/manual/html_node/Preparing-Strings.html) gives the same rule for its catalogues: translate entire sentences, and use format strings instead of concatenation.

## 7. Decide between stable keys and source text as the key

Not every system invents names. The choice is who owns the identifier:

| Approach                                  | Example                                  | Strength                                                | Cost                                                                    |
| ----------------------------------------- | ---------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
| Semantic keys (i18next, Rails I18n)       | `t('summary.title', { ns: 'checkout' })` | Copy edits don&#39;t touch code; keys document location | Someone names, files, and prunes every key                              |
| Source text as the key (gettext)          | `_("Order summary")`                     | Code stays readable; no naming step                     | An English edit creates a new entry; same-text conflicts need `msgctxt` |
| Generated IDs from inline text (FormatJS) | `defaultMessage="Order summary"`         | Message lives with its usage; IDs are automatic         | Needs a build plugin and an extraction step                             |

Use semantic keys when copy is managed outside the codebase (for example, a content team editing strings in a TMS) or when web and mobile apps share one catalogue. Use source text or inline messages when developers write the copy and you want code review to show the words users see.

## 8. Keep keys out of URLs, analytics, and logic

Keys and translated strings are for display. Don&#39;t send `t('checkout:cta')` as an analytics event name (each locale would report a different event), don&#39;t build a route slug from a key, and don&#39;t branch on `label === t('status.open')`. Use a fixed event name, a slug you control, and an enum or status code. Then renaming a key or editing a translation can&#39;t break a dashboard, a link, or a condition.

## 9. Clean up unused and missing keys automatically

Catalogues drift: keys stay after their component is deleted, and new keys ship untranslated. Run a checker in CI.

* **i18next**: [`i18next-cli`](https://github.com/i18next/i18next-cli) replaces the deprecated `i18next-parser`. On a small test project, `npx i18next-cli status --unused` reported an orphaned `oldPromoBanner` key without changing any files; `npx i18next-cli extract` then removed it (its `removeUnusedKeys` option defaults to `true`) and wrote empty German entries for every key, including `itemCount_one` and `itemCount_other`. `npx i18next-cli status` exits with an error while translations are missing, and `rename-key` renames a key across source and locale files. Static analysis can&#39;t see keys built at run time, such as ``t(`status.${code}`)``, so `extract` treats them as unused and deletes them. List those prefixes in the `preservePatterns` option (for example, `'status.*'`), or spell each key out in code.
* **Rails**: i18n-tasks reports missing and unused keys from your Ruby and ERB code. (See [General Translation vs. i18n-tasks](/kb/general-translation-vs-i18n-tasks).)

## How General Translation handles keys

With the [`<T>`](/docs/react/reference/components/t) component in `gt-react` or `gt-next`, the English stays in the component and there&#39;s no key to name:

```tsx title="CheckoutSummary.tsx"
import { T } from 'gt-react';

export function CheckoutSummary() {
  return (
    <T>
      <h2>Order summary</h2>
      <small>Tax is calculated at checkout.</small>
    </T>
  );
}
```

[`npx gt translate`](/docs/cli/reference/commands/translate) extracts the content still present in your source, so removing a component removes the need to maintain its keys by hand. Distinct uses of &quot;Open&quot; still need context: `<T context="Ticket status">Open</T>` or `gt('Open', { $context: 'Ticket status' })` with [`useGT`](/docs/react/reference/hooks/use-gt). An optional [`id`](/docs/react/reference/components/t#id) prop on [`<T>`](/docs/react/reference/components/t), or a `$id` option on the translation function, gives an entry a stable identifier for the translation editor. (See [Can I do i18n without translation files?](/kb/i18n-without-translation-files).)

Teams that want keys can keep them. Write one source dictionary, such as `dictionary.json`, and read it with [`useTranslations`](/docs/react/reference/hooks/use-translations); the same command translates it, and looking up an id that isn&#39;t in the dictionary throws instead of rendering a raw key. (See [Translating with dictionaries](/docs/react/guides/translating-with-dictionaries).) Existing i18next or next-intl JSON files are translated as they are, and a companion [`.metadata.json` file](/docs/cli/reference/keyed-metadata) adds per-key context or a character limit, the keyed equivalent of convention 3.

## Key takeaways

* Name keys by location and meaning (`checkout.summary.title`), never by wording or position (`text_47`).
* Give &quot;Open&quot; the verb and &quot;Open&quot; the status separate keys; share keys only for identical meanings.
* Follow the library&#39;s plural suffixes exactly: i18next uses `_one`, `_other`, and `_zero`, and ICU libraries keep plurals in one message.
* Run `i18next-cli status --unused` or i18n-tasks in CI so dead and missing keys don&#39;t pile up.
* General Translation&#39;s [`<T>`](/docs/react/reference/components/t) needs no keys, and its dictionary mode with [`useTranslations`](/docs/react/reference/hooks/use-translations) keeps them for teams that want them.

## Related questions

- [Can I do i18n without translation files?](https://generaltranslation.com/en-GB/kb/i18n-without-translation-files.mdx): Yes. General Translation's gt-react and gt-next libraries do React and Next.js i18n with no translation keys and no JSON file per language to maintain. Copy stays inline in JSX inside the `<T>` component, and the gt translate command generates each language's translations in your build, with context from your code. Locadex can keep them up to date through pull requests.
- [General Translation vs. i18n-tasks: which should you use?](https://generaltranslation.com/en-GB/kb/general-translation-vs-i18n-tasks.mdx): i18n-tasks is a Ruby gem that finds missing and unused Rails I18n keys, tidies locale files, and can fill missing keys through Google Translate, DeepL, OpenAI, Yandex, or watsonx. General Translation translates whole locale files with your Glossary, Custom Prompts, and Dashboard review. They do different jobs, and many teams can use both.
- [i18next alternatives: General Translation vs. i18next](https://generaltranslation.com/en-GB/kb/general-translation-vs-i18next.mdx): Choose General Translation to keep copy inline in React components and generate translations with its JSX structure and product context. You can also keep i18next and use General Translation to translate existing catalogues. i18next supplies message rendering and plural selection; translation generation comes from a separate service.
- [What is ICU message format?](https://generaltranslation.com/en-GB/kb/icu-message-format.mdx): ICU message format is a syntax for one translatable string that holds variables, plurals, select choices, and ordinals, such as {count, plural, one {# file} other {# files}}. Each language keeps the CLDR plural categories it needs: English uses one and other, while Polish adds few and many.
- [How do I find hardcoded strings that aren't translated?](https://generaltranslation.com/en-GB/kb/find-hardcoded-strings.mdx): Run a linter that flags user-facing string literals in CI, such as eslint-plugin-i18next's no-literal-string rule for JSX, Android Lint's HardcodedText check, or erb_lint's HardCodedString linter for Rails. Then click through the app in a pseudo-locale to catch text that static checks can't trace.

## Previous and next questions

- Previous question: [How do I translate SRT subtitles without breaking timing?](https://generaltranslation.com/en-GB/kb/translate-srt-subtitles.mdx)
- Next question: [Internationalisation vs. localisation (i18n vs. l10n)](https://generaltranslation.com/en-GB/kb/internationalization-vs-localization.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/en-GB/kb/sitemap.md) for every article.
