Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b1fea68
docs(kb): separate catalog generation from plural completeness
pie575 Oct 19, 2026
967c4f2
docs(kb): isolate Rails catalogs with a repeatable YAML adapter
pie575 Oct 19, 2026
ba69e6d
docs(kb): make the Mobility integration reject partial and stale writes
pie575 Oct 19, 2026
40d2083
docs(kb): compare Weblate and GT on the same gettext workflow
pie575 Oct 19, 2026
e5a8c83
docs(kb): scope the Paraglide benchmark to its measured comparison
pie575 Oct 19, 2026
2e6cd15
docs(kb): qualify Locize automatic translation prerequisites and timing
pie575 Oct 19, 2026
e5ad7d2
docs(kb): distinguish Intlayer fill configuration from execution
pie575 Oct 19, 2026
be50dd0
docs(kb): preserve reviewed catalogs during the Symfony YAML migration
pie575 Oct 19, 2026
d55ba26
docs(kb): compare completed Polish renders for Intlayer and Paraglide
pie575 Oct 19, 2026
e1b3069
docs(kb): keep Paraglide rendering with compatible GT catalogs
pie575 Oct 19, 2026
fcea64d
docs(kb): compare complete build and buy localization workflows
pie575 Oct 19, 2026
d20d473
docs(kb): apply KB defect audit to comparison follow-up
pie575 Oct 19, 2026
80dab5f
docs(kb): carry comparison positioning through the follow-up
pie575 Oct 19, 2026
2b5d49b
docs(kb): align the follow-up Paraglide recommendation
pie575 Oct 19, 2026
46beaaa
docs(kb): synchronize the reviewed comparison copy
pie575 Oct 19, 2026
eb0f761
docs(kb): restore the GT argument in the Weblate comparison
pie575 Oct 19, 2026
d165283
docs(kb): clarify the build failure instruction
pie575 Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions kb/general-translation-vs-i18n-tasks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ Use the English-to-Spanish catalog setup from the [Rails integration](/kb/genera

```bash
npx gt translate
ruby script/fix_locale_roots.rb # rename en: to each file's locale
bundle exec i18n-tasks missing # keys used in code, plus plural forms
ruby script/rails_catalogs.rb export
bundle exec i18n-tasks missing
bundle exec i18n-tasks unused
bundle exec i18n-tasks check-consistent-interpolations # %{name} matches the source
bundle exec i18n-tasks check-consistent-interpolations
```

Each `i18n-tasks` command exits with status 1 on a problem, so the job fails before a broken file merges. The CLI needs a project ID and an API key with **Files > Write**, **Context > Write**, and **Translation queue > Enabled** permissions, set as `GT_PROJECT_ID` and `GT_API_KEY`. (See [Generating translations](/docs/cli/guides/generating-translations).)
You can configure the build to stop if translation or validation fails. After adding or changing a plural message, also render representative target-language counts: key completeness and matching placeholders do not establish correct grammar. The CLI needs a project ID and an API key with **Files > Write**, **Context > Write**, and **Translation queue > Enabled** permissions, set as `GT_PROJECT_ID` and `GT_API_KEY`. (See [Generating translations](/docs/cli/guides/generating-translations).)

## Key takeaways

Expand Down
30 changes: 14 additions & 16 deletions kb/general-translation-vs-in-house-i18n.mdx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
---
title: Should you build your own i18n or use General Translation?
summary: Hand-rolled i18n (a dictionary per language and a small t() helper) is fine for one or two languages and a little copy. As an app grows, you rebuild plural rules, fallbacks, and file syncing, and someone still writes every translation. General Translation's open-source libraries handle the run-time work, and General Translation generates the translations from your code.
summary: Compare the full workflow before building your own i18n. A small helper can suit stable copy; an established library can connect to automated translation; General Translation combines inline authoring with generation and review. The decision is which rendering, translation, and release responsibilities your team wants to own.
date: 2026-10-16
category: internationalization
authors: [kevin-wu, jackie]
related: [general-translation-vs-i18n-libraries, how-to-handle-plurals-in-react, multilingual-nextjs-seo, i18n-without-translation-files, general-translation-vs-google-translate]
---

You can build internationalization (i18n) yourself: a messages object per language, a `t()` helper that looks up a key and fills in variables, a locale switcher, and the built-in `Intl` APIs for numbers and dates. For one or two languages and a few dozen strings, that's often all you need, and it's what a coding agent will often write when you ask it to add a language. What grows with the app is everything around the helper: plural rules, missing keys, keeping every language file in step with the code, and producing every translation in the first place. General Translation's open-source libraries do the run-time half of that work, and General Translation generates and updates the translations from your code.
Building your own i18n means owning more than a lookup helper: message selection, formatting, fallback behavior, translation production, and release checks. A small dictionary can be enough for stable copy. An established library plus a translation service is another complete option. General Translation combines its libraries with generation and review, so developers can keep source copy inline while the service maintains target translations.

## When hand-rolled i18n is enough

- **No dependencies.** A dictionary and a twenty-line helper add nothing to your bundle, and you control every line.
- **No third-party runtime dependency.** A dictionary and a small helper still add their own bytes, but you control that code and its loading strategy.
- **The platform does the formatting.** `Intl.NumberFormat`, `Intl.DateTimeFormat`, `Intl.PluralRules`, and `Intl.Locale` are [built into modern browsers and Node.js](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) and format currencies, dates, and plural categories per locale.
- **Small, stable copy.** A prototype, an internal tool, or a site with one extra language that a teammate writes doesn't need more.
- **Languages that pluralize like English.** English and German both use only [`one` and `other`](https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html).

## What breaks as it grows

Here's a typical first version, with a Polish dictionary added:
Here's a minimal helper with two deliberate gaps: an English-only plural rule and no missing-key handling. These are avoidable design errors, not proof that every custom implementation fails:

```js title="i18n.mjs"
const messages = {
Expand Down Expand Up @@ -115,8 +115,8 @@ export function Inbox({ name, count }: { name: string; count: number }) {
<h1>Welcome back, <Var>{name}</Var></h1>
<Plural
n={count}
one={<p>You have <Num>{count}</Num> unread message.</p>}
other={<p>You have <Num>{count}</Num> unread messages.</p>}
singular={<p>You have <Num>{count}</Num> unread message.</p>}
plural={<p>You have <Num>{count}</Num> unread messages.</p>}
/>
</T>
);
Expand All @@ -141,7 +141,9 @@ The bigger change is that General Translation produces the translations:
- **Reviewed by people.** Your team edits translations in the Dashboard. (See [Reviewing and editing translations](/docs/platform/dashboard/guides/reviewing-translations).)
- **Direction and routing.** [`useLocaleDirection`](/docs/react/reference/hooks/use-locale-direction) returns `'rtl'` for Arabic, and `gt-next` can serve each locale at its own URL with its [middleware](/docs/react/nextjs/app-router-middleware).

The JavaScript libraries are MIT-licensed and work without a General Translation account when you supply translations. Use the hosted service to generate translations, with [usage-based pricing](https://generaltranslation.com/pricing).
The generic `plural` branch above gives GT's JSX translation pipeline source text for target categories such as Polish `few` and `many`; runtime selection then uses the translated branches. That differs from generic JSON translation, which preserves a catalog's source keys. Review the generated forms and render representative counts before release: branch selection alone cannot establish translation quality.

The tradeoff is adopting GT's library and workflow. Its JavaScript libraries are MIT-licensed and can render supplied translations without an account; hosted generation is [billed by usage](https://generaltranslation.com/pricing). Bundled translations do not require runtime calls to GT. An existing library connected to a translation service can also generate, review, and deliver catalogs automatically, so compare that complete option with GT's inline workflow.

At the other end of the scale, Profound ships its platform and docs in 47+ locales with General Translation. (See [production deployments](https://generaltranslation.com/enterprise).)

Expand All @@ -150,11 +152,11 @@ At the other end of the scale, Profound ships its platform and docs in 47+ local
| | Hand-rolled dictionary and helper | Open-source library, such as i18next | General Translation |
| --- | --- | --- | --- |
| Dependencies | None | The library plus framework bindings, such as `react-i18next` | `gt-react` or `gt-next`, plus the `gt` CLI |
| Plurals | Your selection code, and every form in every file | Selected with CLDR rules; you write each form in each catalog | [`<Plural>`](/docs/react/reference/components/plural) selects with CLDR rules; the branches you write are translated with the sentence |
| Plurals | Your selection code and complete target-language forms | CLDR selection; translators or connected automation supply target forms | [`<Plural>`](/docs/react/reference/components/plural) selects with CLDR rules; JSX generation uses generic source branches for target categories |
| Numbers, dates, currencies | `Intl` APIs you call yourself | Built-in formatters on top of `Intl` | [`<Num>`](/docs/react/reference/components/num), [`<Currency>`](/docs/react/reference/components/currency), and [`<DateTime>`](/docs/react/reference/components/datetime) |
| Missing translations | Whatever your helper does | [Falls back to a configured language, then shows the key](https://www.i18next.com/principles/fallback) | Renders the source content |
| Keeping languages in sync | By hand or with your own script | Extraction tools add new keys; someone fills them in | Each [`npx gt translate`](/docs/cli/reference/commands/translate) translates new and changed content |
| Who translates | You or someone you hire | You, translators, or a translation management system | AI with your code's context, Glossary, and Custom Prompts; your team reviews in the Dashboard |
| Keeping languages in sync | Your extraction, generation, and validation process | Extraction plus a connected translation workflow; can run in CI | Each [`npx gt translate`](/docs/cli/reference/commands/translate) generates new and changed content before release |
| Who translates | People or a service you integrate | People, a TMS, or an automated service, including GT for compatible catalogs | AI with marked JSX context, Glossary, and Custom Prompts; your team reviews in the Dashboard |
| Cost | Your time | Free library; translation is separate | Free JavaScript libraries; [usage-based translation](https://generaltranslation.com/pricing) |

## When to build it yourself
Expand All @@ -168,14 +170,10 @@ You don't have to rewrite your helper to stop translating by hand. Move each lan
```json title="gt.config.json"
{
"defaultLocale": "en",
"locales": [
"es"
],
"locales": ["es"],
"files": {
"json": {
"include": [
"locales/[locale].json"
]
"include": ["locales/[locale].json"]
}
}
}
Expand Down
17 changes: 14 additions & 3 deletions kb/general-translation-vs-intlayer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,27 @@ export function Inbox({ count }: { count: number }) {
<h1>Inbox</h1>
<Plural
n={count}
one={<p>You have <Num>{count}</Num> unread message.</p>}
other={<p>You have <Num>{count}</Num> unread messages.</p>}
singular={<p>You have <Num>{count}</Num> unread message.</p>}
plural={<p>You have <Num>{count}</Num> unread messages.</p>}
/>
</section>
</T>
);
}
```

There's no content file and no key. [`npx gt translate`](/docs/cli/reference/commands/translate) sends each [`<T>`](/docs/react/reference/components/t) block, with its element structure, to the General Translation API and writes translations for every target locale to separate files, so adding a locale changes `gt.config.json`, not your components. You write the plural forms English uses, and [`<Plural>`](/docs/react/reference/components/plural) picks a branch with the reader's locale's rules. [`npx gt validate`](/docs/cli/reference/commands/validate) checks the project without calling the API, and [`gt generate`](/docs/cli/reference/commands/generate) writes templates for teams that translate by hand.
There's no authored content declaration or key. [`npx gt translate`](/docs/cli/reference/commands/translate) sends each [`<T>`](/docs/react/reference/components/t) block with its element structure and writes separate target-locale files. The generic `plural` branch supplies source content for target categories such as Polish `few` and `many`; [`<Plural>`](/docs/react/reference/components/plural) selects the translated category at runtime. This JSX workflow differs from translating a JSON catalog with fixed keys.

Both implementations need completed target translations before the Polish inbox is ready. With the same Polish wording loaded into Intlayer 9.7.0 and `gt-react` 11.4.12, the rendered unread-message text is:

| Count | Intlayer | GT |
| --- | --- | --- |
| 1 | Masz 1 nieprzeczytaną wiadomość. | Masz 1 nieprzeczytaną wiadomość. |
| 2 | Masz 2 nieprzeczytane wiadomości. | Masz 2 nieprzeczytane wiadomości. |
| 5 | Masz 5 nieprzeczytanych wiadomości. | Masz 5 nieprzeczytanych wiadomości. |
| 22 | Masz 22 nieprzeczytane wiadomości. | Masz 22 nieprzeczytane wiadomości. |

These are rendering checks using manually supplied translations, not a comparison of AI translation quality. For GT, configure `pl` as a target locale, generate translations, review the result, and load that locale's output through [`GTProvider`](/docs/react/reference/components/gt-provider). [`gt generate`](/docs/cli/reference/commands/generate) creates templates, while [`gt validate`](/docs/cli/reference/commands/validate) checks project structure without translating it. A successful source-only render is not a completed Polish workflow.

Code context helps GT translate the meaning of a component. Each marked block includes its text, element structure, and explicit context notes; a note can distinguish saving a document from saving money. [Context Groups](/docs/platform/dashboard/guides/defining-context-for-translations) keep your Glossary and Custom Prompts consistent across assigned projects. Reviewers edit translations in the Dashboard, and CI or Locadex keeps them current as copy changes. [Automatic JSX injection](/docs/cli/guides/using-auto-jsx) can add the wrappers during the build when enabled.

Expand Down
Loading