diff --git a/kb/general-translation-vs-i18n-tasks.mdx b/kb/general-translation-vs-i18n-tasks.mdx index 3bb160a8e..cadb70241 100644 --- a/kb/general-translation-vs-i18n-tasks.mdx +++ b/kb/general-translation-vs-i18n-tasks.mdx @@ -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 diff --git a/kb/general-translation-vs-in-house-i18n.mdx b/kb/general-translation-vs-in-house-i18n.mdx index 1e749dc72..d09baca9e 100644 --- a/kb/general-translation-vs-in-house-i18n.mdx +++ b/kb/general-translation-vs-in-house-i18n.mdx @@ -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 = { @@ -115,8 +115,8 @@ export function Inbox({ name, count }: { name: string; count: number }) {

Welcome back, {name}

You have {count} unread message.

} - other={

You have {count} unread messages.

} + singular={

You have {count} unread message.

} + plural={

You have {count} unread messages.

} /> ); @@ -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).) @@ -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 | [``](/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 | [``](/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` | [``](/docs/react/reference/components/num), [``](/docs/react/reference/components/currency), and [``](/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 @@ -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"] } } } diff --git a/kb/general-translation-vs-intlayer.mdx b/kb/general-translation-vs-intlayer.mdx index c577d8d66..d86aed7f7 100644 --- a/kb/general-translation-vs-intlayer.mdx +++ b/kb/general-translation-vs-intlayer.mdx @@ -92,8 +92,8 @@ export function Inbox({ count }: { count: number }) {

Inbox

You have {count} unread message.

} - other={

You have {count} unread messages.

} + singular={

You have {count} unread message.

} + plural={

You have {count} unread messages.

} /> @@ -101,7 +101,18 @@ export function Inbox({ count }: { count: number }) { } ``` -There's no content file and no key. [`npx gt translate`](/docs/cli/reference/commands/translate) sends each [``](/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 [``](/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 [``](/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`; [``](/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. diff --git a/kb/general-translation-vs-mobility.mdx b/kb/general-translation-vs-mobility.mdx index dac0db128..32e7ee4af 100644 --- a/kb/general-translation-vs-mobility.mdx +++ b/kb/general-translation-vs-mobility.mdx @@ -43,44 +43,81 @@ That's the right behavior for a storage layer: Mobility saved the English values ## Filling Mobility attributes with General Translation -General Translation's runtime translation endpoint, `POST https://api.gtx.dev/v2/translate`, translates strings on demand. It takes a project API key with **Runtime translation** permission as a bearer token, and a body with a `requests` object keyed by your own IDs, each with a `source` string and optional `metadata` such as a `context` note, plus `sourceLocale`, `targetLocale`, and a top-level `metadata` object (it can be empty). The response is keyed by the same IDs. A small Ruby method covers it: +Keep Mobility as the store for published content. General Translation's [runtime endpoint](/docs/platform/openapi/reference/translation/translate-runtime) supplies translated strings using your project's Glossary and Custom Prompts. The Rails application decides when those strings become visible. + +Use a project API key with **Runtime translation** permission on the server. Treat a failed field as a failed batch: publishing a translated title beside an untranslated body is usually worse than leaving the previous version intact. ```ruby title="app/services/gt_translate.rb" require "net/http" require "json" -# Translates a hash of { field => text } with General Translation's runtime API. -# Returns { field => translation }, with nil for any entry that failed. -def gt_translate(fields, from:, to:, context: nil) - uri = URI("https://api.gtx.dev/v2/translate") - requests = fields.to_h do |field, text| - [field.to_s, { source: text, metadata: { dataFormat: "STRING", context: context }.compact }] +class TranslationFailure < StandardError; end + +class GtTranslate + def self.call(fields, from:, to:, context:) + uri = URI("https://api.gtx.dev/v2/translate") + requests = fields.to_h do |field, text| + raise TranslationFailure, "Missing source field: #{field}" unless text.is_a?(String) + [field.to_s, { source: text, metadata: { dataFormat: "STRING", context: context } }] + end + request = Net::HTTP::Post.new(uri) + request["Authorization"] = "Bearer #{ENV.fetch("GT_API_KEY")}" + request["Content-Type"] = "application/json" + request["gt-api-version"] = "2026-03-06.v1" + request.body = { requests:, sourceLocale: from, targetLocale: to, metadata: {} }.to_json + response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, + open_timeout: 5, read_timeout: 30, write_timeout: 10) do |http| + http.request(request) + end + raise TranslationFailure, "Translation HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess) + + results = JSON.parse(response.body) + requests.keys.to_h do |field| + result = results.fetch(field) + unless result["success"] == true && result["translation"].is_a?(String) + raise TranslationFailure, "Translation failed for #{field}" + end + [field, result.fetch("translation")] + end end +end +``` - request = Net::HTTP::Post.new(uri) - request["Authorization"] = "Bearer #{ENV.fetch("GT_API_KEY")}" # project key with Runtime translation permission - request["Content-Type"] = "application/json" - request["gt-api-version"] = "2026-03-06.v1" - request.body = { requests:, sourceLocale: from.to_s, targetLocale: to.to_s, metadata: {} }.to_json +An HTTP success does not mean every field succeeded. This method rejects partial results, missing fields, and malformed responses before any database write. It reports status or field names without putting the response body into logs. - response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) } - raise "Translation failed (#{response.code}): #{response.body}" unless response.is_a?(Net::HTTPSuccess) +For a background job, capture both the English source and the current target values when enqueueing. Disable Mobility fallbacks when reading those snapshots, so an empty Spanish field stays distinguishable from English fallback text: - JSON.parse(response.body).transform_values { |result| result["success"] ? result["translation"] : nil } -end -``` +```ruby title="app/jobs/translate_post_job.rb" +class TranslatePostJob < ApplicationJob + def self.snapshot(post, locale) + Mobility.with_locale(locale.to_sym) do + { "title" => post.title(fallback: false), "body" => post.body(fallback: false) } + end + end -```ruby -translated = gt_translate({ title: post.title, body: post.body }, - from: :en, to: :es, context: "Blog post for an online shoe store") -Mobility.with_locale(:es) { post.update!(translated.compact) } + def perform(post_id, locale, source, previous_target) + raise ArgumentError, "Cannot overwrite English" if locale == "en" + post = Post.find(post_id) + return unless self.class.snapshot(post, "en") == source && + self.class.snapshot(post, locale) == previous_target + + translated = GtTranslate.call(source, from: "en", to: locale, + context: "Blog post for an online shoe store") + post.with_lock do + post.reload + return unless self.class.snapshot(post, "en") == source && + self.class.snapshot(post, locale) == previous_target + Mobility.with_locale(locale.to_sym) { post.update!(translated) } + end + end +end ``` -Each entry comes back as `{ "success": true, "translation": "..." }` or `{ "success": false, "error": "...", "code": ... }`, so one failed field doesn't lose the others. (See the [endpoint reference](/docs/platform/openapi/reference/translation/translate-runtime) for every field.) +Enqueue this job with `TranslatePostJob.snapshot(post, "en")` and `TranslatePostJob.snapshot(post, "es")` captured under the same post lock, after the source edit commits. Pass the same snapshots on retries. Allow only your configured target locales, and have every editor and importer acquire that parent-row lock before changing translated attributes. This contract matters because Mobility can store attributes in separate rows. -General Translation does more here than a raw machine translation call: the endpoint translates with your project's Glossary and Custom Prompts, so product names and tone match the rest of your product, and it caches results, so translating the same text again is served from the cache. Call it from a background job after a record is saved, keep the API key on the server, and expect `429` responses above the [rate limits](/docs/platform/openapi/overview#api-basics). If part of your stack runs on Node.js, [`tx`](/docs/node/reference/functions/tx) in `gt-node` and the [Core library](/docs/platform/core/guides/translating-strings) do the same runtime translation from JavaScript. +The job does the network work outside the lock, then checks again before writing. A source edit or a reviewer edit made while translation is running makes the result stale, so the job leaves it unapplied. A repeated delivery cannot overwrite the translation already saved by its earlier attempt; if the generated values already matched the previous target, repeating that identical write is harmless. Superseded work should enqueue a fresh snapshot. Configure bounded retries for timeouts, `429`, and server failures in your job runner; investigate authentication and invalid-content failures instead of retrying them indefinitely. See the [API rate limits](/docs/platform/openapi/overview#api-basics). -The tradeoff: these translations go straight into your database the moment the job runs. If editors must approve a translation before readers see it, add that step yourself, for example by saving machine translations as drafts. +This example publishes both fields together. If approval is required, save the result as a draft tied to its source snapshot and check that snapshot again when approving it. GT's Dashboard file-review workflow does not approve Mobility database rows. The application's draft and publication rules remain yours, while GT supplies context-aware translations. Node.js services can use [`tx`](/docs/node/reference/functions/tx) for the same runtime endpoint. ## Mobility vs. General Translation at a glance diff --git a/kb/general-translation-vs-nestjs-i18n.mdx b/kb/general-translation-vs-nestjs-i18n.mdx index a934ac8ad..c3ee88851 100644 --- a/kb/general-translation-vs-nestjs-i18n.mdx +++ b/kb/general-translation-vs-nestjs-i18n.mdx @@ -43,7 +43,7 @@ The Spanish and Polish folders hold the same file, translated by hand with the s ?lang=pl&count=3 {"one":"Masz 3 nieprzeczytaną wiadomość.","other":"Masz 3 nieprzeczytanej wiadomości."} ``` -German silently falls back to English, because nobody wrote that catalog. Polish is worse: `Intl.PluralRules` picks `few` for 3 and `many` for 5, the Polish file only has the `one` and `other` keys copied from English, and nestjs-i18n returns the whole plural object instead of a sentence. The fix is to know which plural categories each language needs and write them. That's the work nestjs-i18n leaves to you: producing every catalog, in every language, with the right forms, and redoing it each time the English changes. +German falls back to English because its catalog is absent. In Polish, `Intl.PluralRules` picks `few` for 3 and `many` for 5; this incomplete catalog makes nestjs-i18n return the plural object. The catalog needs the target language's forms. ## How General Translation fits a NestJS app @@ -65,7 +65,7 @@ General Translation's CLI translates nestjs-i18n's catalogs where they are. Poin } ``` -[`npx gt translate`](/docs/cli/reference/commands/translate) reads `src/i18n/en/inbox.json` and writes `src/i18n/es/inbox.json`, `src/i18n/pl/inbox.json`, and `src/i18n/de/inbox.json`, keeping every key. It checks that each translation keeps the source's `{name}` and `{count}` placeholders. Run it in CI, and entries that haven't changed reuse their earlier translations. The CLI doesn't know nestjs-i18n's syntax specifically (its library-aware mode covers next-intl and i18next), so review strings that use `$t()` references. +[`npx gt translate`](/docs/cli/reference/commands/translate) reads `src/i18n/en/inbox.json` and writes Spanish and German catalogs, keeping the keys. These languages use `one` and `other` for the integer counts in this example. Entries that haven't changed reuse earlier translations. The CLI's library-aware syntax handling covers next-intl and i18next, so exercise nestjs-i18n's `{count}` interpolation and `$t()` references in the application before adding generation to CI. This example translates an English catalog into Spanish. Use this file workflow for messages with matching source and target key structures, and validate representative plural counts in the rendered app. diff --git a/kb/general-translation-vs-paraglide.mdx b/kb/general-translation-vs-paraglide.mdx index 9cba24af3..2cb9a3a20 100644 --- a/kb/general-translation-vs-paraglide.mdx +++ b/kb/general-translation-vs-paraglide.mdx @@ -76,8 +76,8 @@ export function Inbox({ name, count }: { name: string; count: number }) {

Hello {name}!

You have {count} unread message.

} - other={

You have {count} unread messages.

} + singular={

You have {count} unread message.

} + plural={

You have {count} unread messages.

} />
@@ -85,7 +85,15 @@ export function Inbox({ name, count }: { name: string; count: number }) { } ``` -There's no message file to keep in step: the English is the component. [`npx gt translate`](/docs/cli/reference/commands/translate) sends each [``](/docs/react/reference/components/t) block, with its element structure, to the General Translation API and writes a translation for every target locale, so a new sentence is translated before your build instead of falling back to English. A value in [``](/docs/react/reference/components/var) is never sent to the API, and [``](/docs/react/reference/components/plural) picks a branch with the reader's locale's plural 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 template files for teams that translate by hand. +The English stays in the component. [`npx gt translate`](/docs/cli/reference/commands/translate) generates locale files from each marked block and its element structure. Configure `pl`, run generation, review the output, and load the Polish translations through [`GTProvider`](/docs/react/reference/components/gt-provider). The generic `plural` source branch supplies content for target categories such as `few` and `many`. A value in [``](/docs/react/reference/components/var) stays out of the translation API request. + +For an equivalent finished comparison, restore Paraglide's Polish `greeting` and give both implementations the same complete Polish messages. With Paraglide 2.29.0 and `gt-react` 11.4.12, count 2 renders: + +```html title="Output from both completed Polish catalogs" +

Skrzynka odbiorcza

Cześć Ada!

Masz 2 nieprzeczytane wiadomości.

+``` + +Both also select the singular wording at 1, `nieprzeczytanych wiadomości` at 5, and `nieprzeczytane wiadomości` at 22. These checks use manually supplied translations to compare rendering, not AI quality. Paraglide can generate missing messages through inlang tooling; GT provides its own generation and Dashboard review workflow. [`gt generate`](/docs/cli/reference/commands/generate) creates templates and [`gt validate`](/docs/cli/reference/commands/validate) checks structure; neither command proves that the target copy is translated. GT uses code context to translate the content as a whole: text, nested elements, and branches inside each marked block travel together. Explicit notes explain the product meaning, and a shared [Context Group](/docs/platform/dashboard/guides/defining-context-for-translations) applies Glossary terms and Custom Prompts across assigned projects. Reviewers edit in the Dashboard; CI or Locadex keeps translations aligned with source changes. @@ -110,12 +118,43 @@ Keep Paraglide JS when you build with SvelteKit, Astro, or Solid, or when per-me ## Using Paraglide JS and General Translation together -The `gt` CLI doesn't read inlang projects. It can pick up `messages/[locale].json` as ordinary JSON, but it treats every string value as copy: plain messages such as `Hello {name}!` keep their placeholders, while a variant message's `declarations` and `selectors` entries would be sent as text, and target locales would get only the English-shaped variant keys. So don't point the CLI at a Paraglide project that uses variants. (See the [JSON file format](/docs/cli/reference/formats/json-files) for how the CLI handles JSON from other libraries.) +You can keep Paraglide's compiler and typed `m.*` functions while using GT to generate compatible catalog files. The boundary is the file format: GT does not import `project.inlang` or understand the default format's `declarations`, `selectors`, and `match` metadata. Sending a native variant file through generic JSON translation would treat that metadata as copy and preserve the source-shaped branches. + +Paraglide also accepts [format plugins](https://paraglidejs.com/file-formats). For example, this i18next plugin configuration compiles a flat catalog containing `inboxTitle` and `greeting: "Hello {{name}}!"`: + +```json title="project.inlang/settings.json" +{ + "baseLocale": "en", + "locales": ["en", "es"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@6.1.4/dist/index.js" + ], + "plugin.inlang.i18next": { + "pathPattern": "./locales/{locale}.json" + } +} +``` + +```json title="gt.config.json" +{ + "defaultLocale": "en", + "locales": ["es"], + "files": { + "json": { + "include": ["locales/[locale].json"] + } + } +} +``` + +The GT CLI targets the catalog files, then Paraglide compiles the translated output. With Paraglide 2.29.0 and plugin 6.1.4, a supplied Spanish catalog containing `"Bandeja de entrada"` and `"¡Hola {{name}}!"` compiled successfully; `m.greeting({ name: 'Ada' }, { locale: 'es' })` returned `¡Hola Ada!`. This establishes file and compiler compatibility for those simple messages, not automatic migration of native variants or generated translation quality. + +Import existing reviewed translations before generating new ones with [`gt upload`](/docs/cli/reference/commands/upload). After generation, check placeholders, run Paraglide's compiler and inlang's completeness checks, and render the target messages. Keep native variant catalogs out of this generic JSON path. Changing plugins also changes the expected catalog format, so convert and validate existing messages before switching a populated project. -Moving one component at a time works. Paraglide's messages are plain functions and need no provider, so an `m.*` call and a [``](/docs/react/reference/components/t) block can render side by side while you convert routes. [Migrating i18n libraries](/docs/react/guides/migrating-i18n-libraries) covers the steps, starting from the [React Quickstart](/docs/react/react-quickstart) or the [Next.js Quickstart](/docs/react/nextjs-quickstart). +Move components to [``](/docs/react/reference/components/t) when you want inline authoring and marked JSX structure as translation context. Paraglide functions and GT components can render side by side during that move; see [Migrating i18n libraries](/docs/react/guides/migrating-i18n-libraries). A rendering-library migration is optional for compatible catalogs. ## Key takeaways - Paraglide JS compiles messages into typed, tree-shakable functions, with plural support, locale strategies, and separate inlang tooling for generation and checks. - General Translation keeps copy inline in [``](/docs/react/reference/components/t) and generates translations with context from your code, your Glossary, and Custom Prompts, with review in the Dashboard. -- Paraglide wins on per-message bundle size, typed messages, and framework reach beyond React and Vue; the `gt` CLI doesn't understand inlang variants, so migrate component by component instead of translating Paraglide's files. +- Keep Paraglide's typed functions and tree-shaking when they fit your app. GT can generate compatible catalogs without replacing the compiler; native inlang variants need a separate workflow. Move to inline GT components when their authoring and context advantages are the reason to switch. diff --git a/kb/general-translation-vs-rails-i18n.mdx b/kb/general-translation-vs-rails-i18n.mdx index e334f83e7..72ae4b911 100644 --- a/kb/general-translation-vs-rails-i18n.mdx +++ b/kb/general-translation-vs-rails-i18n.mdx @@ -1,6 +1,6 @@ --- title: "General Translation vs. Rails I18n: do you need both?" -summary: Rails I18n is the translation API built into Ruby on Rails. It looks up keys in YAML files and formats dates and numbers, but every translation is yours to write. General Translation has no Ruby SDK, so it doesn't replace Rails I18n; its CLI translates the YAML files Rails reads, with one rename step. +summary: Keep Rails I18n for Ruby lookups and formatting, and add General Translation to generate compatible YAML catalogs with shared terminology and Dashboard review. A YAML adapter separates GT's catalogs from Rails' locale-rooted runtime files. date: 2026-10-16 category: internationalization authors: [kevin-wu, jackie] @@ -56,56 +56,83 @@ pl, count 5: Masz 5 nieprzeczytanych wiadomości. pl, count 22: Masz 22 nieprzeczytanych wiadomości. ``` -Counts 2 and 22 need "nieprzeczytane wiadomości", and nothing warns you. [i18n-tasks](/kb/general-translation-vs-i18n-tasks) catches it (its `missing` command lists `few, many` for that key), but someone still has to write every form, in every language, and keep each file current as the English changes. +Counts 2 and 22 need "nieprzeczytane wiadomości". [i18n-tasks](/kb/general-translation-vs-i18n-tasks) checks that the catalog includes the required plural categories. Render representative counts as well to check that each form reads correctly. ## Translating Rails locale files with General Translation -Keep Rails I18n for lookups, and let General Translation write the other locale files. The `gt` CLI runs on Node.js, translates YAML, and keeps your keys. Point `gt.config.json` at your source file: +Keep Rails I18n for rendering and use GT to generate compatible catalogs. The adapter below maps between rootless translation files and the locale-rooted files Rails loads. Keep the translation files in `localization/rails` and the generated runtime files in `config/locales`. + +The following adapter owns one catalog per locale. It imports existing translations once and exports the runtime files after each translation run. It parses YAML, rejects unexpected shapes and missing files, and prepares every output before writing any of them. + +```ruby title="script/rails_catalogs.rb" +require "json" +require "yaml" +require "fileutils" + +config = JSON.parse(File.read("gt.config.json")) +locales = ([config.fetch("defaultLocale")] + config.fetch("locales")).uniq +abort "Invalid locale" unless locales.all? { |locale| /\A[a-zA-Z0-9-]+\z/.match?(locale) } +FileUtils.mkdir_p("localization/rails") + +read_catalog = lambda do |path| + value = YAML.safe_load_file(path, aliases: false) + raise "Expected a message mapping: #{path}" unless value.is_a?(Hash) + value +end + +case ARGV.fetch(0) +when "import" + locale = ARGV.fetch(1) + abort "Locale is not configured" unless locales.include?(locale) + path = "localization/rails/#{locale}.yml" + abort "Catalog already imported: #{path}" if File.exist?(path) + rooted = read_catalog.call("config/locales/#{locale}.yml") + abort "Expected exactly the #{locale} root" unless rooted.keys == [locale] && rooted[locale].is_a?(Hash) + File.write(path, YAML.dump(rooted.fetch(locale))) +when "export" + outputs = locales.to_h do |locale| + messages = read_catalog.call("localization/rails/#{locale}.yml") + ["config/locales/#{locale}.yml", YAML.dump({ locale => messages })] + end + outputs.each do |path, yaml| + File.write("#{path}.tmp", yaml) + File.rename("#{path}.tmp", path) + end +else + abort "Usage: ruby script/rails_catalogs.rb import LOCALE | export" +end +``` ```json title="gt.config.json" { "defaultLocale": "en", - "locales": [ - "es" - ], + "locales": ["es"], "files": { "yaml": { - "include": [ - "config/locales/[locale].yml" - ] + "include": ["localization/rails/[locale].yml"] } } } ``` -Set `GT_PROJECT_ID` and `GT_API_KEY` as described in [Generating translations](/docs/cli/guides/generating-translations). Run [`npx gt translate`](/docs/cli/reference/commands/translate), then map the generated Spanish catalog to Rails' `es:` root with the script below. Treat translation and root mapping as one build step, and load the catalogs only after both commands complete. +Import English and any existing Spanish catalog before the first run: -```ruby title="script/fix_locale_roots.rb" -# Run after `npx gt translate`: renames the copied root key (en:) in each -# translated file to the file's own locale (es:, pl:, and so on). -require "json" - -config = JSON.parse(File.read("gt.config.json")) -source = config.fetch("defaultLocale") - -config.fetch("locales").each do |locale| - next if locale == source - - path = "config/locales/#{locale}.yml" - next unless File.exist?(path) - - yaml = File.read(path, encoding: "UTF-8") - File.write(path, yaml.sub(/^(["']?)#{Regexp.escape(source)}\1:/, "#{locale}:")) -end +```bash +ruby script/rails_catalogs.rb import en +# Run this if a reviewed Spanish catalog already exists: +ruby script/rails_catalogs.rb import es ``` +Edit source copy in `localization/rails/en.yml` from then on. Configure [credentials](/docs/cli/guides/generating-translations), and run [`gt upload`](/docs/cli/reference/commands/upload) once to import the source and existing translations before generating more. Keep GT's files and lockfile in version control; treat `config/locales/en.yml` and `es.yml` as generated outputs, and avoid defining the same keys in other Rails catalogs. + ```bash -npx gt translate && ruby script/fix_locale_roots.rb +npx gt upload +npx gt translate && ruby script/rails_catalogs.rb export ``` -The script only touches the unindented root line, leaves comments and formatting alone, and does nothing to a file that's already correct, so it's safe to run every time. +For later local review, edit the downloaded files in `localization/rails` and run [`gt save-local`](/docs/cli/reference/commands/save-local) before translating again. Dashboard edits are another review path. In CI, run the second line and stop the build if either command fails. The exporter replaces each file atomically, but a filesystem failure can still leave only some locales updated; publish only after the whole command succeeds. YAML serialization normalizes formatting and drops comments. For multiple catalogs per locale, extend the explicit input/output mapping before using this adapter. -That extra step buys you more than a raw translation API would. General Translation translates each file with your project's [Glossary and Custom Prompts](/docs/platform/dashboard/guides/defining-context-for-translations), so a product name or a formal tone holds across every locale file, and your team can [review and edit translations](/docs/platform/dashboard/guides/reviewing-translations) in the Dashboard. Unchanged entries reuse earlier translations, so running the CLI in CI on every merge only translates what changed. +General Translation applies your project's [Glossary and Custom Prompts](/docs/platform/dashboard/guides/defining-context-for-translations), and reviewers can [edit translations in the Dashboard](/docs/platform/dashboard/guides/reviewing-translations). Repeated translation runs reuse unchanged entries, while the adapter keeps the file shape stable for local review and upload. This example translates an English catalog into Spanish. Use this file workflow for messages with matching source and target key structures, and validate representative plural counts in the rendered app. Run `i18n-tasks check-consistent-interpolations` to check Rails `%{count}` placeholders before release. @@ -128,4 +155,4 @@ Use Rails I18n on its own when translators or a vendor already deliver your loca - Rails I18n renders translations from `config/locales` and formats dates and numbers; rails-i18n adds plural rules and locale data. Neither writes translations. - General Translation has no Ruby SDK. Its CLI translates the YAML files Rails reads, using your Glossary and Custom Prompts. -- Map each translated catalog to its Rails locale root after [`npx gt translate`](/docs/cli/reference/commands/translate), then load the completed files in the application. +- Keep GT's catalogs outside Rails' load path and export locale-rooted runtime files after translation. Review the stable GT catalogs, then regenerate Rails' copies. diff --git a/kb/general-translation-vs-weblate.mdx b/kb/general-translation-vs-weblate.mdx index 3457788cb..6e79afeed 100644 --- a/kb/general-translation-vs-weblate.mdx +++ b/kb/general-translation-vs-weblate.mdx @@ -1,13 +1,13 @@ --- title: "General Translation vs. Weblate: which should you use?" -summary: Weblate is a GPL-licensed, Git-based localization platform you can self-host for free or use as Hosted Weblate, which hosts public libre projects at no charge. It commits translations back to your repository, but engineers still extract strings, wait for translators, and merge results. General Translation translates content in your code with its context and ships every language with each release. +summary: Choose General Translation to make translation part of development and releases, with code context for inline React content, shared terminology, and built-in review. Keep your existing gettext catalogs or write copy directly in components. Weblate fits projects organized around volunteer translation or a self-hosted translation platform. date: 2026-10-16 category: workflow-tools authors: [kevin-wu, jackie] related: [general-translation-vs-tms, general-translation-vs-gettext, general-translation-vs-tolgee, general-translation-vs-django-i18n, best-localization-software] --- -Weblate is an open-source, web-based translation platform built around Git. You connect a repository, Weblate reads the translation files in it, translators and volunteers translate in its editor, and Weblate commits the results and pushes them back or opens a pull request. It's a common home for open-source projects with community translators. What Weblate leaves to engineers is everything around the repository sync: extracting strings into translation files, waiting for each language, and merging and deploying the results. General Translation starts from the code instead, and translates as part of the build. +General Translation connects translation generation to the code changes you ship. Run its CLI in CI, or use [Locadex](/docs/platform/locadex/quickstart) to prepare translation changes as pull requests. Your team reviews translations in the Dashboard, and the release includes the translated copy. For a Weblate project, you can start with the gettext files you already use. ## What Weblate does well @@ -20,35 +20,7 @@ Weblate is an open-source, web-based translation platform built around Git. You - **Quality checks.** [Dozens of checks](https://docs.weblate.org/en/latest/user/checks.html) cover placeholders, missing plural forms, maximum length, ICU MessageFormat syntax, and unchanged translations, and automatic fixups correct whitespace and ellipses. - **Community translation.** Volunteers can suggest translations and comment, reviewers approve them, and a project can [commit only approved strings](https://docs.weblate.org/en/latest/workflows.html). Screenshots, glossaries, and source references give translators context. -## What Weblate still leaves to your engineers - -Here's a Python app using gettext, with a Spanish catalog translated before the last line was added: - -```python title="app.py" -import gettext -t = gettext.translation("messages", localedir="locale", languages=["es"]) -_ = t.gettext -print(_("Inbox")) -print(_("No messages yet.")) # new copy, added after the last translation round -``` - -After extracting the POT template, translating only "Inbox" in `locale/es/LC_MESSAGES/messages.po`, and compiling it with Babel 2.17.0, a Spanish reader sees the new line in English: - -```text title="Output" -Bandeja de entrada -No messages yet. -``` - -That line stays in English until a round trip finishes: - -1. Wrap the string in `_()` and regenerate the POT template with `xgettext` or `pybabel extract`, then commit it. -2. Weblate picks up the change from a webhook or its next fetch, and its `msgmerge` add-on adds the new entry to each PO file. -3. Wait for volunteers, translators, or machine translation to fill in each language, and for a reviewer if you require approval. -4. Weblate commits the changes (by default, [pending changes are committed after 24 hours](https://github.com/WeblateOrg/weblate/blob/main/weblate/trans/defaults.py) unless something triggers a commit sooner) and pushes them or opens a pull request. You merge it, compile MO files, and deploy. - -Edits to the same files outside Weblate can cause merge conflicts, which [Weblate's docs suggest avoiding](https://docs.weblate.org/en/latest/admin/continuous.html#avoiding-merge-conflicts-by-locking-weblate-while-doing-outside-changes) by locking the component while you change files. And a translator sees each string with its source references and screenshots, not the code that renders it. - -Self-hosting is free. Hosted Weblate is priced by hosted strings, which are source strings times the number of languages plus one, and every plan includes unlimited languages, projects, and translators. (See [Weblate hosting](https://weblate.org/en/hosting/), checked October 2026.) +Self-hosting Weblate is free. Hosted Weblate charges by hosted strings; see its [hosting plans](https://weblate.org/en/hosting/) for current allowances. ## Weblate vs. General Translation at a glance @@ -56,33 +28,40 @@ Self-hosting is free. Hosted Weblate is priced by hosted strings, which are sour | --- | --- | --- | | Where strings live | Translation files in your repository, mirrored in Weblate components | In your code inside [``](/docs/react/reference/components/t), or in the resource files you already have | | How code connects | Weblate pulls from Git, then pushes commits or opens pull requests | [`npx gt translate`](/docs/cli/reference/commands/translate) in your build, or [Locadex](/docs/platform/locadex/quickstart) pull requests | -| Who translates | Volunteers, your translators, machine translation, or LLMs | AI with context from your code, Glossary, and Custom Prompts, reviewed by your team in the Dashboard | +| Who translates | Volunteers, your translators, machine translation, or LLMs | AI with inline code context or catalog content and metadata, plus assigned Glossary and Custom Prompts; your team reviews in the Dashboard | | Context for each string | Source references, screenshots, glossary, and translation memory | Inline blocks carry their marked element structure and context notes; catalogs carry file content and metadata. Assigned Context Groups supply Glossary and Custom Prompts | -| File formats | About 50, including XLIFF, ARB, Fluent, and Java properties | JSON, YAML, PO, Markdown and MDX, iOS and Android strings, and more; no XLIFF, ARB, Fluent, or properties | +| File formats | About 50, including XLIFF, ARB, Fluent, and Java properties | JSON, YAML, PO, Markdown and MDX, iOS and Android strings, and more | | Hosting | Self-hosted for free, or Hosted Weblate | Hosted platform; open-source libraries and CLI | | Pricing | Self-hosted free; hosted plans by hosted strings; free Libre plan for public libre projects | [Usage-based translation](https://generaltranslation.com/pricing) | | License | GPL-3.0-or-later | MIT-licensed CLI and JavaScript libraries | ## Why teams choose General Translation over Weblate -General Translation starts from the code, so the round trip above goes away: copy stays in your components inside [``](/docs/react/reference/components/t), and translation runs with each release. - -- **Faster.** [`npx gt translate`](/docs/cli/reference/commands/translate) runs in your build, or Locadex opens a pull request when code changes, so new copy ships translated with the release that adds it instead of waiting for a commit window. Adding a language is one line in `gt.config.json`, and the next build translates the whole project into it. -- **Cheaper to run.** There's no server to install, upgrade, or back up, and engineers don't extract templates or resolve translation merge conflicts. -- **Higher quality.** In GT's inline workflow, text, elements, variables, and branches inside each marked block are translated together. Explicit notes clarify meaning, and [Context Groups](/docs/platform/dashboard/guides/defining-context-for-translations) keep terminology and tone consistent across assigned projects. Teams retaining catalogs get the same shared Glossary and Custom Prompts alongside their file metadata. -- **Lower risk.** No second system commits to the same files, so there are no translation merge conflicts to avoid by locking components. General Translation's JavaScript libraries and CLI are open source under the MIT license and work without an account, and translations can live as files in your repository, so you can change how translations are produced without rewriting your app. +GT brings generation, context, and review into your development workflow. In React and Next.js, copy stays readable inside [``](/docs/react/reference/components/t), with text, elements, variables, and branches translated together. For an existing gettext app, the CLI generates the PO files your application already loads. -The tradeoff: General Translation's platform is a hosted service, and it's built for AI translation reviewed by your team, not for a community of volunteer translators. +- **Faster.** Run [`npx gt translate`](/docs/cli/reference/commands/translate) before your build to generate new and changed content and reuse existing translations. Or configure Locadex to open a pull request when code changes. Translation follows your release process, so new copy can ship translated with the feature that introduces it. +- **Cheaper to run.** Moving from self-hosted Weblate removes a translation server from your team's installation, upgrade, and backup responsibilities. GT also includes unlimited reviewer seats, so you can involve translators, product managers, and regional reviewers without per-seat charges. See [pricing](https://generaltranslation.com/pricing) for usage rates. +- **Higher quality.** Inline component structure and developer-provided notes give GT the context to distinguish a document's “Save” action from “Save money.” For catalog workflows, file content and metadata accompany your assigned [Glossary and Custom Prompts](/docs/platform/dashboard/guides/defining-context-for-translations). Share those instructions across the app, docs, and marketing site to keep product terminology consistent, then refine translations in the [Dashboard editor](/docs/platform/dashboard/guides/reviewing-translations). +- **Lower risk.** Adopt GT without rewriting your gettext application: keep its message calls, PO files, and MO compilation, and add generation to CI. Translations remain files you can review and version in your repository. GT's CLI and JavaScript libraries are MIT-licensed, and the gettext app continues to load its compiled translations locally. -Profound's platform and docs run in 47+ locales on General Translation. Because the platform is hosted rather than self-hosted, its [trust center](https://trust.inc/generaltranslation) documents its SOC 2 Type II and ISO 27001 certifications and GDPR compliance. +[Profound uses General Translation](https://generaltranslation.com/enterprise) across its platform and docs, an example of using one translation platform for both product and documentation. ## When Weblate is the better fit -Choose Weblate when an open-source project wants volunteers to translate it, when you need to self-host the whole platform under a copyleft license, or when you work in formats General Translation doesn't support, such as XLIFF, ARB, Fluent, Java properties, PHP arrays, or Qt Linguist files. Choose General Translation when most of your product's text lives in React, Next.js, or Vue code, or in JSON, YAML, PO, or Markdown files, and you want every release to ship in every language. +Choose Weblate when volunteer participation is central to your project, you want to operate the translation platform yourself, or its XLIFF, ARB, Fluent, and other format integrations fit your existing toolchain. Its community workflows, self-hosting, and broad format support are established strengths. ## Using both on gettext files -General Translation's CLI translates the same gettext files Weblate reads. Point a `pot` entry at your template and map each output to the folder layout Weblate uses: +You can add GT's generation to an existing gettext project while keeping Weblate for community translation. The application keeps its message calls and catalog layout. Here's where each platform fits: + +| Step | Weblate | General Translation | +| --- | --- | --- | +| Extract | Regenerate the POT template and commit it; a webhook triggers synchronization | Regenerate the POT template before running the GT CLI | +| Translate | An automatic-translation add-on or people fill the PO entries, with configured checks and review | [`gt translate`](/docs/cli/reference/commands/translate) generates PO entries using assigned terminology and prompts; reviewers can edit them in the Dashboard | +| Deliver | Commit and push translations, or open a pull request, according to the project's workflow | Download translations in CI or commit reviewed outputs to the repository | +| Release | Compile PO files to MO files, test, and deploy | Compile PO files to MO files, test, and deploy | + +Point a `pot` entry at your template and map each output to the folder layout Weblate uses: ```json title="gt.config.json" { @@ -105,10 +84,10 @@ With `gt` 2.26.5, this config maps `locale/messages.pot` to `locale/es/LC_MESSAG ## Is General Translation an alternative to Weblate? -Yes, for teams whose text lives in React, Next.js, or Vue code, or in JSON, YAML, PO, or Markdown files. General Translation translates in your build instead of waiting on a commit window. Weblate remains the better fit for volunteer translation, a fully self-hosted platform, or formats such as XLIFF and ARB. +Yes. Choose General Translation when you want translation generated alongside development, guided by product context, and reviewed in one platform. You can keep existing PO, JSON, or YAML catalogs, or use inline authoring in React, Next.js, and Vue. For a gettext project, start with the POT configuration above; for a React app, start with the [React quickstart](/docs/react/react-quickstart). ## Key takeaways -- Weblate is a strong copyleft platform for open-source and community translation, with Git-based continuous localization, free self-hosting, and about 50 file formats. -- Engineers still extract strings into translation files, wait for each language, and merge Weblate's commits before readers see new copy translated. -- General Translation translates content where it's written, with the context of the code, and ships every language with each release, on MIT-licensed JavaScript libraries with no lock-in. +- Choose General Translation to connect translation generation and review to the releases you already ship, with shared product terminology across your app and docs. +- Keep your existing gettext application and catalogs, or use inline components to give GT the structure and context of your React UI. +- Weblate remains a fit for community-led translation and self-hosting. You can introduce GT language by language while keeping volunteer translation in Weblate.