From b1fea6828389ed0aee0407946581853d7c9c7c78 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:30:04 -0700 Subject: [PATCH 01/14] docs(kb): separate catalog generation from plural completeness --- kb/general-translation-vs-i18n-tasks.mdx | 4 ++-- kb/general-translation-vs-nestjs-i18n.mdx | 8 ++++---- kb/general-translation-vs-rails-i18n.mdx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kb/general-translation-vs-i18n-tasks.mdx b/kb/general-translation-vs-i18n-tasks.mdx index 7e790e210..e2ee7c677 100644 --- a/kb/general-translation-vs-i18n-tasks.mdx +++ b/kb/general-translation-vs-i18n-tasks.mdx @@ -72,7 +72,7 @@ Choose i18n-tasks alone when translators write your locale files and you need CI ## Using both in CI -Run General Translation first, then let i18n-tasks check the result: +For catalogs compatible with the configured GT file path, run generation first, then validate the result. This is a release gate, not a plural-repair step: an English-shaped Polish catalog still fails until its `few` and `many` forms are supplied through a workflow that retains them on subsequent runs. ```bash npx gt translate @@ -82,7 +82,7 @@ bundle exec i18n-tasks unused bundle exec i18n-tasks check-consistent-interpolations # %{name} matches the source ``` -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).) +Configure the CI runner to stop on a nonzero exit status. Each `i18n-tasks` check then blocks a merge when it finds a problem. 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-nestjs-i18n.mdx b/kb/general-translation-vs-nestjs-i18n.mdx index 912a78125..91c1613f7 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. This is also a boundary for GT's generic JSON translation: it translates existing keys, so sending the English-shaped object alone does not repair the Polish catalog. ## How General Translation fits a NestJS app @@ -52,7 +52,7 @@ General Translation's CLI translates nestjs-i18n's catalogs where they are. Poin ```json title="gt.config.json" { "defaultLocale": "en", - "locales": ["es", "pl", "de"], + "locales": ["es", "de"], "files": { "json": { "include": ["src/i18n/[locale]/*.json"] @@ -61,9 +61,9 @@ 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. -Plurals need planning. The CLI builds each translated catalog from the English file's structure, so a plural object gets the categories English has, `one` and `other`. General Translation doesn't add Polish `few` and `many`, and keys you add by hand to one language are dropped when that file is translated again. One way to handle it is to list every category your languages need in the English file (English never selects `few` or `many`), which also keeps `nestjs-i18n check` passing, and then review those forms in the Dashboard. +Polish plural catalogs need a different generation strategy: this JSON path does not add `few` and `many`. Adding them only to a generated target file is insufficient because a later run rebuilds its structure from the source. Keep plural-heavy catalogs that need extra categories outside this generic translation path until their generation and update workflow preserves those forms. Use GT for compatible catalogs and dynamic content, with the project's Glossary, Custom Prompts, and review workflow. For content that only exists at run time, such as a teammate's post or a database record, use General Translation's [`gt-node`](/docs/node/quickstart). Initialize it once, then pass nestjs-i18n's resolved language to [`withGT`](/docs/node/reference/functions/with-gt) before calling [`tx`](/docs/node/reference/functions/tx). This example assumes the existing `I18nModule` has its catalogs and [resolvers](https://nestjs-i18n.com/concepts/resolver) configured, and `PostsService` returns the post's English source text. diff --git a/kb/general-translation-vs-rails-i18n.mdx b/kb/general-translation-vs-rails-i18n.mdx index 5e993f731..89cee6b54 100644 --- a/kb/general-translation-vs-rails-i18n.mdx +++ b/kb/general-translation-vs-rails-i18n.mdx @@ -56,7 +56,7 @@ 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) catches the missing categories. This is a catalog-completeness problem: a translation service must supply the right forms as well as translated words. GT's generic YAML path preserves source keys, so translating an English-shaped file alone does not fix this Polish example. ## Translating Rails locale files with General Translation @@ -65,7 +65,7 @@ Keep Rails I18n for lookups, and let General Translation write the other locale ```json title="gt.config.json" { "defaultLocale": "en", - "locales": ["es", "pl"], + "locales": ["es"], "files": { "yaml": { "include": ["config/locales/[locale].yml"] @@ -103,7 +103,7 @@ The script only touches the unindented root line, leaves comments and formatting 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. -Two tradeoffs. General Translation translates the plural forms your English file has, so it won't add Polish `few` and `many` keys that `en.yml` doesn't contain. And its docs note that custom string syntax is supported for next-intl and i18next, so treat Rails `%{name}` placeholders as something to check: `i18n-tasks check-consistent-interpolations` fails CI when a translation's placeholders don't match the source. +This example targets Spanish, whose integer plural categories fit the English `one` and `other` structure. Adding Polish requires a separate solution for its additional categories; GT's generic YAML translation does not create them. Keep those messages in a workflow that preserves their full target-language structure. Also check Rails `%{name}` interpolation: [custom syntax support](/docs/cli/reference/formats/yaml-files) is library-aware for next-intl and i18next, and `i18n-tasks check-consistent-interpolations` catches mismatched placeholders. GT's benefit here is contextual generation and review of compatible catalogs, not automatic repair of every Rails message format. ## Rails I18n vs. General Translation at a glance From 967c4f2100040e4ab9faa11d582e3a497f7df615 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:34:28 -0700 Subject: [PATCH 02/14] docs(kb): isolate Rails catalogs with a repeatable YAML adapter --- kb/general-translation-vs-rails-i18n.mdx | 83 ++++++++++++++++-------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/kb/general-translation-vs-rails-i18n.mdx b/kb/general-translation-vs-rails-i18n.mdx index 89cee6b54..0ff33c082 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] @@ -60,7 +60,48 @@ Counts 2 and 22 need "nieprzeczytane wiadomości". [i18n-tasks](/kb/general-tran ## 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. Rails expects a locale root such as `es:`; GT's generic YAML workflow preserves source keys. Keep GT's catalogs outside Rails' load path, without that root, and generate the rooted files Rails reads. This prevents a downloaded Spanish file with an `en:` root from replacing English at runtime. + +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" { @@ -68,42 +109,32 @@ Keep Rails I18n for lookups, and let General Translation write the other locale "locales": ["es"], "files": { "yaml": { - "include": ["config/locales/[locale].yml"] + "include": ["localization/rails/[locale].yml"] } } } ``` -With a project ID and API key set as `GT_PROJECT_ID` and `GT_API_KEY` ([Generating translations](/docs/cli/guides/generating-translations) covers credentials), running [`npx gt translate`](/docs/cli/reference/commands/translate) sends `config/locales/en.yml` to the General Translation API and writes `es.yml` and `pl.yml`. One thing to know: the CLI keeps every key, including the root, so `es.yml` comes back rooted at `en:`. Rails merges every file in `config/locales` by its root key, so the translated files overwrite your English: the English inbox renders in whichever translated file Rails loads last. Rename the root after each run: +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 targets Spanish, whose integer plural categories fit the English `one` and `other` structure. Adding Polish requires a separate solution for its additional categories; GT's generic YAML translation does not create them. Keep those messages in a workflow that preserves their full target-language structure. Also check Rails `%{name}` interpolation: [custom syntax support](/docs/cli/reference/formats/yaml-files) is library-aware for next-intl and i18next, and `i18n-tasks check-consistent-interpolations` catches mismatched placeholders. GT's benefit here is contextual generation and review of compatible catalogs, not automatic repair of every Rails message format. +This example targets Spanish, whose integer plural categories fit the English `one` and `other` structure. GT's generic YAML path does not add Polish `few` and `many` keys. Keep messages requiring extra categories in a workflow that preserves them. Run `i18n-tasks check-consistent-interpolations` and render representative counts to check Rails `%{count}` placeholders and plural behavior before release. ## Rails I18n vs. General Translation at a glance @@ -124,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. -- The CLI keeps the `en:` root key in translated files, which overwrites English in Rails, so rename it after each [`npx gt translate`](/docs/cli/reference/commands/translate) with a short script like the one above. +- 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. From ba69e6d0095ee60950b71706bf9d741d40e665d8 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:35:06 -0700 Subject: [PATCH 03/14] docs(kb): make the Mobility integration reject partial and stale writes --- kb/general-translation-vs-mobility.mdx | 85 ++++++++++++++++++-------- 1 file changed, 61 insertions(+), 24 deletions(-) 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 From 40d208375ae2dd7b29b7064b553971826e3ee18e Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:35:47 -0700 Subject: [PATCH 04/14] docs(kb): compare Weblate and GT on the same gettext workflow --- kb/general-translation-vs-weblate.mdx | 54 ++++++++++++++------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/kb/general-translation-vs-weblate.mdx b/kb/general-translation-vs-weblate.mdx index 971e5c36b..4fa4afaf1 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: Weblate and General Translation both automate translation of existing catalogs. Choose Weblate for community participation, self-hosting, and broad file support. Choose GT for hosted generation and review connected to your build, with shared terminology across product surfaces and an inline option for React. 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. +Weblate is an open-source translation platform built around Git, with human and automatic translation, review, and repository synchronization. General Translation generates translations through a CLI and hosted service, with Dashboard review. For an existing gettext app, both can produce the same PO files; the choice is how you want generation, review, and delivery to fit your team. ## What Weblate does well @@ -20,35 +20,39 @@ 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 +## How each workflow handles new copy -Here's a Python app using gettext, with a Spanish catalog translated before the last line was added: +Consider the same Python gettext app in both workflows: ```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 +print(_("No messages yet.")) ``` -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: +If the Spanish catalog contains only the first message, either workflow renders: -```text title="Output" +```text title="Output before translating the new message" Bandeja de entrada No messages yet. ``` -That line stays in English until a round trip finishes: +That fallback shows an incomplete catalog. Both platforms can automate the work that closes the gap; neither removes Python's extraction, MO compilation, or deployment steps. -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. +| Step | Weblate | General Translation | +| --- | --- | --- | +| Extract | Regenerate the POT template and commit it; a webhook triggers synchronization | Regenerate the same 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 the 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 the PO files to MO files, test, and deploy | Compile the same PO files to MO files, test, and deploy | + +Weblate batches pending edits using lazy commits, but its default pending-change interval is not a mandatory release delay: [repository operations and explicit commits can flush changes earlier](https://docs.weblate.org/en/latest/vcs.html). Both workflows need a clear owner for each catalog when people or automation edit files concurrently. -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. +For this Python example, GT receives the catalog and its metadata. Switching translation providers does not automatically send the surrounding Python source. GT's additional inline JSX context applies when an application uses its React or Next.js libraries. -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 @@ -65,16 +69,16 @@ Self-hosting is free. Hosted Weblate is priced by hosted strings, which are sour ## 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. +For the gettext app above, GT offers a hosted generation and review workflow that fits directly into CI. Use the POT configuration below, run translation before MO compilation, and make successful validation a condition of 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. -- **Context for quality.** In GT's inline workflow, text, elements, and branches inside a marked block travel together during translation. Catalog translation uses the file's content and metadata instead. Assigned Context Groups supply Glossary terms and Custom Prompts to either workflow; they apply across the projects you assign. -- **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. +- **Translation in the release workflow.** The CLI generates new and changed content during CI and reuses unchanged translations. Weblate can also automate translation; GT's appeal here is running generation alongside the build with the same CLI used for other product content. +- **Shared product terminology.** Assigned Context Groups apply Glossary terms and Custom Prompts across the app, docs, and marketing projects. Reviewers edit generated file translations in the Dashboard. +- **Less platform administration.** GT runs the translation service. That removes the installation, upgrade, and backup work of self-hosted Weblate; Hosted Weblate also removes that administration. +- **An inline option for React.** In a React or Next.js app, GT can translate marked JSX with its element and branch structure. That is a separate integration choice from translating this Python app's existing PO files. -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. +Keep catalog changes in your normal pull request and release checks. GT-generated files can still conflict with concurrent edits, so automation needs ownership rules just as Weblate does. -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 across its platform and docs. See [production deployments](https://generaltranslation.com/enterprise) and [pricing](https://generaltranslation.com/pricing). Choose Weblate when volunteer coordination or running the whole translation platform yourself matters most. ## When Weblate is the better fit @@ -105,10 +109,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, for supported catalogs such as PO, JSON, and YAML, and for teams choosing GT's inline React workflow. Both platforms can automate translation. GT combines hosted generation, shared terminology, and Dashboard review with a CLI that runs in your build. Weblate offers volunteer workflows, self-hosting, and broader file-format support. ## 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. +- Weblate combines Git synchronization, automatic and human translation, review, and community participation, with free self-hosting. +- For the same gettext app, both workflows still extract messages, produce PO files, compile MO files, and deploy. Configure automation and review before comparing release time. +- GT adds hosted translation generation and shared product context to existing catalogs. Its inline JSX workflow offers richer structural context when you use its React libraries. From e5a8c8381763f24dabbb74a007a30db7d947e18b Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:36:24 -0700 Subject: [PATCH 05/14] docs(kb): scope the Paraglide benchmark to its measured comparison --- kb/general-translation-vs-paraglide.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kb/general-translation-vs-paraglide.mdx b/kb/general-translation-vs-paraglide.mdx index f14e24f6e..c6818588a 100644 --- a/kb/general-translation-vs-paraglide.mdx +++ b/kb/general-translation-vs-paraglide.mdx @@ -12,10 +12,10 @@ Paraglide JS is a compiler-first i18n library from inlang. You keep messages in ## What Paraglide JS does well - **Type safety.** Message keys and parameters are generated types. With version 2.29.0, a misspelled `m.greting()` and a call to `m.greeting({})` without its `name` parameter are both TypeScript errors. -- **Bundle size.** Messages compile to ES modules, so unused ones are tree-shaken. Paraglide's [benchmark](https://paraglidejs.com/benchmark) reports i18n bundles 3 to 10 times smaller than runtime libraries, for example 47 KB against 205 KB for i18next with 5 locales and 200 messages. +- **Bundle size.** Messages compile to ES modules, so unused ones are tree-shaken. Paraglide's own [benchmark](https://paraglidejs.com/benchmark) reports 47 KB versus 205 KB for i18next with 5 locales, 100 messages used, and 200 messages available. It measures total page transfer size for its test sites; it does not benchmark `gt-react` or establish the difference for your application. - **Plurals and variants.** The default message format [declares a plural selector](https://paraglidejs.com/variants) and matches on CLDR categories such as `one`, `few`, and `many`. [Plugins](https://paraglidejs.com/file-formats) add ICU MessageFormat 1 and i18next files, and formatters cover numbers, dates, and relative time through `Intl`. - **Locale strategies.** An ordered [`strategy` list](https://paraglidejs.com/strategy) resolves the locale from the URL, a cookie, `localStorage`, the browser's preferred language, a global variable, custom strategies, or the `baseLocale`. In 2.29.0 the [default](https://github.com/opral/paraglide-js/blob/main/src/compiler/compiler-options.ts) is `cookie`, `globalVariable`, then `baseLocale`. [Server middleware](https://paraglidejs.com/middleware) and `localizeUrl` handle localized routes. -- **Framework reach.** Paraglide [works with any Vite app](https://paraglidejs.com/vite) works with React, Vue, Svelte, and Solid, and has guides for SvelteKit, TanStack Start, React Router, Astro, and Next.js. It's the [add-on behind `npx sv add paraglide`](https://paraglidejs.com/sveltekit) in the Svelte CLI. Its [Next.js guide](https://paraglidejs.com/next-js) covers the App Router through a webpack plugin, run with the `--webpack` flag. +- **Framework reach.** Paraglide [works with Vite](https://paraglidejs.com/vite), including React, Vue, Svelte, and Solid apps, and has guides for SvelteKit, TanStack Start, React Router, Astro, and Next.js. It's the [add-on behind `npx sv add paraglide`](https://paraglidejs.com/sveltekit) in the Svelte CLI. Its [Next.js guide](https://paraglidejs.com/next-js) covers the App Router through a webpack plugin, run with the `--webpack` flag. - **Free, open tooling.** The library is [MIT-licensed](https://github.com/opral/paraglide-js/blob/main/LICENSE). The [inlang CLI](https://inlang.com/m/2qj2w8pu/app-inlang-cli) checks projects in CI, [Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) shows translations inline in VS Code, and [Fink](https://fink.inlang.com/) is a free browser editor that commits translators' changes to a GitHub repository. ## What Paraglide JS leaves to you From 2e6cd15568351c4be24be6ea69ae2c3d264ea27c Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:36:24 -0700 Subject: [PATCH 06/14] docs(kb): qualify Locize automatic translation prerequisites and timing --- kb/general-translation-vs-locize.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kb/general-translation-vs-locize.mdx b/kb/general-translation-vs-locize.mdx index 35f5a2d51..8c5365126 100644 --- a/kb/general-translation-vs-locize.mdx +++ b/kb/general-translation-vs-locize.mdx @@ -65,7 +65,7 @@ No messages yet. POST https://api.lite.locize.app/missing/demo-project/latest/en/translation {"inbox.empty":"No messages yet."} ``` -This stub demonstrates key collection and fallback behavior; it doesn't run Locize's translation service. On plans with [automatic translation](https://www.locize.com/docs/automatic-translation), Locize translates new keys within seconds of their arrival. A production workflow connects collection, translation, and delivery: +This stub demonstrates key collection and fallback behavior; it doesn't run Locize's translation service or measure its latency. Locize's [automatic translation](https://www.locize.com/docs/automatic-translation) processes new reference-language keys in the background when enabled, with trial credits, a subscription, or a configured AI/MT provider. Existing keys require a bulk action. Delivery also depends on the project's review and publishing settings. A production workflow connects collection, translation, and delivery: 1. Set up i18next, name a key for each string, and call `t()` with it. 2. Send new keys to Locize with `saveMissing` in development (it's limited to `localhost` by default, and [the write key must never ship](https://www.locize.com/docs/guides/going-to-production)) or with a sync command in CI. From e5ad7d2920487e84513abeda0be937045e387461 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:36:44 -0700 Subject: [PATCH 07/14] docs(kb): distinguish Intlayer fill configuration from execution --- kb/general-translation-vs-intlayer.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kb/general-translation-vs-intlayer.mdx b/kb/general-translation-vs-intlayer.mdx index 24822d08b..9fb9dd60a 100644 --- a/kb/general-translation-vs-intlayer.mdx +++ b/kb/general-translation-vs-intlayer.mdx @@ -76,7 +76,7 @@ Type '{ en: string; }' is missing the following properties from type 'Required>': es, pl ``` -At run time, the missing locales fell back to English. To close the gap, you write the translations or run `intlayer fill`; Intlayer's [`dictionary.fill`](https://intlayer.org/doc/concept/configuration) setting, which defaults to on, governs auto-filling missing translations with AI. Fill sends the dictionary's content, [its description](https://intlayer.org/doc/concept/content), and an `applicationContext` string from your config to the model. It doesn't see the component or the JSX around a string. [Intlayer's compiler](https://intlayer.org/doc/compiler) can extract plain JSX text into content files at build time, so you can skip writing declarations by hand, but translations still end up in those files. +At run time, the missing locales fell back to English. To close the gap, write the translations or run [`intlayer fill`](https://intlayer.org/doc/concept/cli/fill) with a configured provider or Intlayer service access. The [`dictionary.fill`](https://intlayer.org/doc/concept/configuration) default of `true` chooses the source file as the fill destination; it is not evidence that translation ran. A path pattern can write per-locale files, and `false` disables filling. Fill sends dictionary content, [its description](https://intlayer.org/doc/concept/content), and configured application context to the model. [Intlayer's compiler](https://intlayer.org/doc/compiler) can extract JSX text into declarations, reducing manual authoring; generation and review remain steps in the workflow. ## General Translation keeps one language in the code From be50dd0e08fa66ad8583676b1d80ce4d3c831e5f Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:37:11 -0700 Subject: [PATCH 08/14] docs(kb): preserve reviewed catalogs during the Symfony YAML migration --- kb/general-translation-vs-symfony-translation.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kb/general-translation-vs-symfony-translation.mdx b/kb/general-translation-vs-symfony-translation.mdx index 9e1ae10e1..65c6f8135 100644 --- a/kb/general-translation-vs-symfony-translation.mdx +++ b/kb/general-translation-vs-symfony-translation.mdx @@ -112,9 +112,9 @@ This isn't an either-or choice for rendering: a Symfony app should keep Symfony ## Using both -1. Keep the catalogs you want translated in YAML. If they're XLIFF today, Symfony can dump a locale's catalog as YAML with `translation:extract --force --format=yaml`, which writes the YAML next to the XLIFF files; delete the XLIFF files afterward. +1. Keep the catalogs you want translated in YAML. Work on a branch if they are XLIFF today. Run `php bin/console translation:extract --force --format=yaml en`, then repeat with each existing target locale, such as `es`. The locale argument is required. This extracts messages and updates catalogs; it is not a lossless XLIFF archive converter. Compare the resulting YAML against every original locale and domain, including messages the extractor cannot find dynamically, reviewed values, placeholders, and `+intl-icu` messages. Replace generated `__` values with real source copy. Preserve any XLIFF review metadata elsewhere if your team needs it. Only remove an original XLIFF file once its YAML replacement has complete message parity, and test with that XLIFF file absent so it cannot hide a missing YAML entry. Keep the originals in Git history. 2. Add `gt.config.json` as above, and import your source catalogs and existing translations with [`npx gt upload`](/docs/cli/reference/commands/upload) before generating new translations. Compare a known reviewed message in the Dashboard with the committed catalog. -3. Change one English message, run [`npx gt translate`](/docs/cli/reference/commands/translate), and inspect the diff to confirm that unrelated reviewed messages remain intact. Then run translation in CI after extraction. For later local translation edits, opt into [`--save-local`](/docs/cli/reference/commands/translate) or run [`gt save-local`](/docs/cli/reference/commands/save-local) before generating again. +3. Change one English message, run [`npx gt translate`](/docs/cli/reference/commands/translate), and inspect the diff to confirm that unrelated reviewed messages remain intact. Run `php bin/console lint:translations`, inspect `php bin/console debug:translation es` for each target locale, and render representative messages and plural counts. Then run translation in CI after extraction. For later local translation edits, opt into [`--save-local`](/docs/cli/reference/commands/translate) or run [`gt save-local`](/docs/cli/reference/commands/save-local) before generating again. ## Key takeaways From d55ba26e9dd41f0331ee7044ef61389a40bcbb4a Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:40:17 -0700 Subject: [PATCH 09/14] docs(kb): compare completed Polish renders for Intlayer and Paraglide --- kb/general-translation-vs-intlayer.mdx | 17 ++++++++++++++--- kb/general-translation-vs-paraglide.mdx | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/kb/general-translation-vs-intlayer.mdx b/kb/general-translation-vs-intlayer.mdx index 9fb9dd60a..e42e2432e 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. In this inline workflow, each block carries the element structure inside it and any explicit context note. Assigned Context Groups add Glossary terms and Custom Prompts (see [Defining context for translations](/docs/platform/dashboard/guides/defining-context-for-translations)). Reviewers edit in the Dashboard, and the CLI runs in CI, or Locadex opens pull requests for React and Next.js apps. Teams that don't want to mark JSX by hand can turn on [automatic JSX injection](/docs/cli/guides/using-auto-jsx), which is off by default. Intlayer's compiler likewise reduces manual authoring; its output is content declarations. diff --git a/kb/general-translation-vs-paraglide.mdx b/kb/general-translation-vs-paraglide.mdx index c6818588a..149df0ad6 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. In this inline example, the marked block supplies text, element structure, and explicit context to translation. A shared Context Group applies its Glossary and Custom Prompts to assigned projects (see [Defining context for translations](/docs/platform/dashboard/guides/defining-context-for-translations)), and reviewers edit results in the Dashboard. The CLI runs in CI, or Locadex can open pull requests for React apps. @@ -119,3 +127,4 @@ Moving one component at a time works. Paraglide's messages are plain functions a - 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. + From e1b30695bfbec0210d67f6a18d2bb3778a3c65b1 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:40:17 -0700 Subject: [PATCH 10/14] docs(kb): keep Paraglide rendering with compatible GT catalogs --- kb/general-translation-vs-paraglide.mdx | 38 ++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/kb/general-translation-vs-paraglide.mdx b/kb/general-translation-vs-paraglide.mdx index 149df0ad6..9e97d80e5 100644 --- a/kb/general-translation-vs-paraglide.mdx +++ b/kb/general-translation-vs-paraglide.mdx @@ -118,13 +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. -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). +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. + +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. From fcea64df7cb8204fa0807be69893dd00c59bb7a5 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 11:41:17 -0700 Subject: [PATCH 11/14] docs(kb): compare complete build and buy localization workflows --- kb/general-translation-vs-in-house-i18n.mdx | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/kb/general-translation-vs-in-house-i18n.mdx b/kb/general-translation-vs-in-house-i18n.mdx index d09c73f31..2bea89945 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 tradeoff is a dependency and a service: `gt-react` or `gt-next` plus the `gt` CLI, with generated translations billed by usage. The libraries are MIT-licensed and work without a General Translation account, so you can also load translations you write yourself. And translation covers the plural branches you write: from English `one` and `other`, Polish also gets two branches, and counts Polish treats as `few` or `many` render the `other` branch, so check them in review. +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,7 +170,7 @@ 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", "pl", "ar"], + "locales": ["es"], "files": { "json": { "include": ["locales/[locale].json"] @@ -177,7 +179,7 @@ You don't have to rewrite your helper to stop translating by hand. Move each lan } ``` -[`npx gt translate --dry-run`](/docs/cli/reference/commands/translate) lists `locales/en.json` as the source, and a full run writes `locales/es.json`, `locales/pl.json`, and `locales/ar.json`. A companion `locales/en.metadata.json` can add a `context` note or `maxChars` limit per key. (See [JSON](/docs/cli/reference/formats/json-files) and [Keyed metadata](/docs/cli/reference/keyed-metadata).) Two limits apply. The CLI translates values and keeps your keys, so Polish `few` and `many` keys still need adding. And the docs note that custom message syntax, such as ICU plurals, is handled exactly only for next-intl and i18next files, so keep placeholders simple, such as `{name}`. +[`npx gt translate --dry-run`](/docs/cli/reference/commands/translate) lists `locales/en.json` as the source, and a full run writes `locales/es.json`. A companion `locales/en.metadata.json` can add context or character limits per key. See [JSON](/docs/cli/reference/formats/json-files) and [Keyed metadata](/docs/cli/reference/keyed-metadata). This preserves your helper and keys, but does not repair the helper's missing-key behavior. Generic JSON generation also does not add target-only plural keys: keep Polish or Arabic plural messages in a workflow that preserves their full categories. Review custom placeholders and render the actual target output. To move off the helper, wrap one component at a time in [``](/docs/react/reference/components/t), or keep keys with [`useTranslations`](/docs/react/guides/translating-with-dictionaries). If a coding agent is doing the work, give it General Translation's [agent instructions](/docs/overview/for-coding-agents) instead of asking for a `t()` helper. Start with the [Next.js Quickstart](/docs/react/nextjs-quickstart) or the [React SPA Quickstart](/docs/react/react-spa-quickstart). From 2b5d49b2009025b33aa378b198da362e53647bf6 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 14:08:59 -0700 Subject: [PATCH 12/14] docs(kb): align the follow-up Paraglide recommendation --- kb/general-translation-vs-paraglide.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kb/general-translation-vs-paraglide.mdx b/kb/general-translation-vs-paraglide.mdx index d03f717fe..2cb9a3a20 100644 --- a/kb/general-translation-vs-paraglide.mdx +++ b/kb/general-translation-vs-paraglide.mdx @@ -114,7 +114,7 @@ Sierra ships 7+ locales across its web and go-to-market content with General Tra | Frameworks | Vite apps, React, Vue, Solid, SvelteKit, Astro, TanStack Start, React Router, Next.js (webpack) | React, Next.js, Vue, and Node.js; React Native, TanStack Start, and Python are experimental | | Cost | Free and open source | Libraries free; [usage-based translation](https://generaltranslation.com/pricing) | -Keep Paraglide JS when you build with SvelteKit, Astro, or Solid, or when per-message tree-shaking and typed message functions matter most. Its inlang tooling can generate translations too. Use General Translation when you want copy readable in your components, with marked element structure and assigned terminology included during translation, and review in the Dashboard. +Keep Paraglide JS when you build with SvelteKit, Astro, or Solid, or when per-message tree-shaking and typed message functions matter most. Choose General Translation when you want to edit copy directly in components and generate translations with code context before the build, with shared terminology and review in the Dashboard. ## Using Paraglide JS and General Translation together From eb0f7619369fc3df44a494e41fb25c18fc5258d4 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 14:57:31 -0700 Subject: [PATCH 13/14] docs(kb): restore the GT argument in the Weblate comparison --- kb/general-translation-vs-weblate.mdx | 75 +++++++++------------------ 1 file changed, 25 insertions(+), 50 deletions(-) diff --git a/kb/general-translation-vs-weblate.mdx b/kb/general-translation-vs-weblate.mdx index 06ed8d74b..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 and General Translation both automate translation of existing catalogs. Choose Weblate for community participation, self-hosting, and broad file support. Choose GT for hosted generation and review connected to your build, with shared terminology across product surfaces and an inline option for React. +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 translation platform built around Git, with human and automatic translation, review, and repository synchronization. General Translation generates translations through a CLI and hosted service, with Dashboard review. For an existing gettext app, both can produce the same PO files; the choice is how you want generation, review, and delivery to fit your team. +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,38 +20,6 @@ Weblate is an open-source translation platform built around Git, with human and - **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. -## How each workflow handles new copy - -Consider the same Python gettext app in both workflows: - -```python title="app.py" -import gettext -t = gettext.translation("messages", localedir="locale", languages=["es"]) -_ = t.gettext -print(_("Inbox")) -print(_("No messages yet.")) -``` - -If the Spanish catalog contains only the first message, either workflow renders: - -```text title="Output before translating the new message" -Bandeja de entrada -No messages yet. -``` - -That fallback shows an incomplete catalog. Both platforms can automate the work that closes the gap; neither removes Python's extraction, MO compilation, or deployment steps. - -| Step | Weblate | General Translation | -| --- | --- | --- | -| Extract | Regenerate the POT template and commit it; a webhook triggers synchronization | Regenerate the same 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 the 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 the PO files to MO files, test, and deploy | Compile the same PO files to MO files, test, and deploy | - -Weblate batches pending edits using lazy commits, but its default pending-change interval is not a mandatory release delay: [repository operations and explicit commits can flush changes earlier](https://docs.weblate.org/en/latest/vcs.html). Both workflows need a clear owner for each catalog when people or automation edit files concurrently. - -For this Python example, GT receives the catalog and its metadata. Switching translation providers does not automatically send the surrounding Python source. GT's additional inline JSX context applies when an application uses its React or Next.js libraries. - 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 @@ -60,33 +28,40 @@ Self-hosting Weblate is free. Hosted Weblate charges by hosted strings; see its | --- | --- | --- | | 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 -For the gettext app above, GT offers a hosted generation and review workflow that fits directly into CI. Use the POT configuration below, run translation before MO compilation, and make successful validation a condition of release. - -- **Translation in the release workflow.** The CLI generates new and changed content during CI and reuses unchanged translations. Weblate can also automate translation; GT's appeal here is running generation alongside the build with the same CLI used for other product content. -- **Shared product terminology.** Assigned Context Groups apply Glossary terms and Custom Prompts across the app, docs, and marketing projects. Reviewers edit generated file translations in the Dashboard. -- **Less platform administration.** GT runs the translation service. That removes the installation, upgrade, and backup work of self-hosted Weblate; Hosted Weblate also removes that administration. -- **An inline option for React.** In a React or Next.js app, GT can translate marked JSX with its element and branch structure. That is a separate integration choice from translating this Python app's existing PO files. +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. -Keep catalog changes in your normal pull request and release checks. GT-generated files can still conflict with concurrent edits, so automation needs ownership rules just as Weblate does. +- **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 uses General Translation across its platform and docs. See [production deployments](https://generaltranslation.com/enterprise) and [pricing](https://generaltranslation.com/pricing). Choose Weblate when volunteer coordination or running the whole translation platform yourself matters most. +[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" { @@ -109,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 supported catalogs such as PO, JSON, and YAML, and for teams choosing GT's inline React workflow. Both platforms can automate translation. GT combines hosted generation, shared terminology, and Dashboard review with a CLI that runs in your build. Weblate offers volunteer workflows, self-hosting, and broader file-format support. +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 combines Git synchronization, automatic and human translation, review, and community participation, with free self-hosting. -- For the same gettext app, both workflows still extract messages, produce PO files, compile MO files, and deploy. Configure automation and review before comparing release time. -- GT adds hosted translation generation and shared product context to existing catalogs. Its inline JSX workflow offers richer structural context when you use its React libraries. +- 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. From d165283557584a94a78510ec91f841499eadc4b9 Mon Sep 17 00:00:00 2001 From: Jackie Chen Date: Mon, 19 Oct 2026 15:35:53 -0700 Subject: [PATCH 14/14] docs(kb): clarify the build failure instruction --- kb/general-translation-vs-i18n-tasks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kb/general-translation-vs-i18n-tasks.mdx b/kb/general-translation-vs-i18n-tasks.mdx index 25859937a..cadb70241 100644 --- a/kb/general-translation-vs-i18n-tasks.mdx +++ b/kb/general-translation-vs-i18n-tasks.mdx @@ -82,7 +82,7 @@ bundle exec i18n-tasks unused bundle exec i18n-tasks check-consistent-interpolations ``` -Configure the CI runner to stop on a nonzero exit status. Each `i18n-tasks` check then blocks a merge when it finds a problem. 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).) +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