Skip to content

Repository files navigation

polygo

The translation reviewer for your pull requests.
The placeholder that went missing in French, the plural form Polish needs,
{{ modelli }} where the source says {{ models }}:
polygo finds them in milliseconds — zero config, no model, no account.
Install the app and it reviews every pull request. Or four lines of CI. Or one 4 MB binary.
Strings missing? It translates them too, with a local model.

crates.io ci release MIT

polygolint commenting on a pull request: the placeholder name {{user}} was translated to {{usuario}}, with a suggested change and an Apply suggestion button

What lands on a pull request. The suggestion is written by the parser, not a model — one click and the name is back.


Three ways to run it, none of which need configuring

On every pull request — install polygolint on a repository and stop. It reviews the pull requests that touch translation files, comments on the lines that broke, and leaves a committable suggestion where the fix is mechanical. It never blocks a pull request, and it says a thing only once.

In your own CI, if you would rather not install an app — the same review, posted by your own workflow with the repository's token, nothing leaving the runner:

- uses: Na5co/polygo/action@v0
  with:
    mode: check
    review: "true"     # needs pull-requests: write

On your machine, on any repository, including ones you do not own:

curl -fsSL https://raw.githubusercontent.com/Na5co/polygo/main/install.sh | sh
polygo check open-webui/open-webui        # a GitHub repo, cloned shallow and checked

Or brew install na5co/tap/polygo · cargo install polygo · Windows zip. Then polygo completions zsh for tab completion.

polygo check on DuckDuckGo's shipped macOS string catalog: 12 errors, 473 warnings, 0.11 s

Real output on DuckDuckGo's shipped macOS catalog, unmodified. Clone it and run this yourself.


polygo check

polygo check Localizable.xcstrings      # a file (.xcstrings, .strings, strings.xml, .json, .arb, .po, .resx)
polygo check app/src/main/res           # a folder: format and locales are detected
polygo check Dimillian/IceCubesApp      # a GitHub repo (or any git URL): cloned shallow, checked
polygo check                            # the project, once polygo.toml exists

Nothing to configure. It finds the string files your app already has, works out which locales exist and which one is the source, and reports what a user would eventually notice — every finding with the file and line where the fix goes. A polygo.toml exists for when you want to tune it; the checks below run without one:

code severity what it catches
locale warning a locale file sitting next to the others that nothing checks, because its locale is not in target_locales
syntax error a string file that does not parse (a dropped comma, an unclosed <string>) — reported instead of the locale silently vanishing from the run
placeholders error %1$@, {{name}}, %(count)s, {0}, {n, plural, …} missing, added, retyped or reordered — and a translated name ({{ modelli }} for {{ models }}) named as such
plural error a CLDR form the locale needs and doesn't have: few/many for Polish, six for Arabic; ICU, .xcstrings, .stringsdict, <plurals>, msgstr[n], i18next _few
markup error <b>, </a>, <br> the source has and the translation lacks, or the reverse
escape error Android: unescaped ', or a leading @/? that aapt reads as a resource reference
empty error a translation that is blank
identical warning the translation is the English text
fragment warning half translated: ようこそ back!
punctuation warning Settings… became Ajustes; a trailing : or . dropped
whitespace warning a leading/trailing space or newline lost (strings glued together in the UI)
orphan warning a key the locale file has and the source no longer does
state / fuzzy warning marked needs_review in Xcode or #, fuzzy in gettext, shipped anyway
length warning 2.5× the source, or a polygo:max=20 comment exceeded
duplicate error the same key twice in one file; the last one wins silently
array error an Android <string-array> with more or fewer items than the source
glossary error a glossary.toml term translated, or not rendered as required
encoding warning é, ’: a file saved in the wrong encoding
invisible warning zero-width space, mid-string BOM, bidi controls, control characters
link warning a URL or email in the source that the translation changed or dropped
brackets warning ( opened and never closed, when the source keeps it balanced
entities warning &amp;amp;: an entity escaped twice
inconsistent warning the same short term translated two ways in one locale (Réglages ×3, Paramètres ×1)

It ends with the coverage per locale. A translated placeholder name is the one bug with a mechanical repair, so polygo check --fix puts the source name back with no model involved — on a file, a folder or a repo (polygo check owner/repo --fix, then git diff in the clone); everything else --fix re-translates with the configured model. Exit 1 on errors (--strict for warnings too); --json for machines ("fix" carries the corrected text); polygo check --explain punctuation says what a code means and what to do. A key that is right the way it is: polygo:ignore=identical in its comment, or [keys] ignore = { "legal.*" = ["length"] } in polygo.toml.

Adopting it on an old catalog: polygo check --write-baseline accepts everything it finds today into polygo-baseline.json (commit it). From then on check reports and fails only on new findings, and tells you when a known one got fixed so you can prune the file. --no-baseline shows the whole picture.

In CI — one annotation per finding on the PR's Files tab, a table in the job summary, no secrets:

- uses: Na5co/polygo/action@v0
  with:
    mode: check

sarif: "true" also files them in GitHub's Security tab with new/fixed history. Any other CI: polygo check --github or --sarif. Pre-commit: entry: polygo check (snippet).

As a reviewer, review: "true" turns those annotations into inline comments with committable suggestions — the same thing polygolint does, running in your own CI instead. The app is the same code, deployed: bot/ is one container, no database, nothing kept between events.


Found in the wild

polygo check on shipped, production catalogs, unmodified (polygo check <owner/repo> reproduces any of these):

App Found
DuckDuckGo macOS 12 placeholder bugs: Ouvrir dans % @ (a space inside %@), a dropped %5$d, mangled %#@var@
Ice Cubes 44 missing Slavic plural forms; a Ukrainian string reading "ключа DeepL API key"
boringnotch 3,374 strings Xcode marks needs_review, shipped
NewPipe, DuckDuckGo Android stray " that Android silently drops from the UI
Penpot 4 #, fuzzy entries gettext shows in English
Signal iOS French strings with literal <strong> tags the English source doesn't have; Russian one forms without the number, shown for 21, 31, 101 items

Details in KNOWN_BUGS.md. Zero false positives on 3,756 strings from five Android apps.

The same check over 33 popular open-source apps found about a thousand broken placeholders in production, almost all a translator translating the placeholder name in a language the maintainers don't read. That is what polygo check <owner/repo> --fix repairs mechanically; the fixes are landing upstream:

Project Fix Status
Open WebUI 22 placeholders in 13 locales ({{ modelli }} for {{ models }}, {model}}) merged
Dashy {brukernavn} for {username} merged
AppFlowy 14 translated placeholder names in 10 locales open
Cal.diy 10 placeholder names in de, es, et, ro open
Umami {ব্রাউজার} for {browser}, [[9000]]] for {time} open
DuckDuckGo macOS 12 broken %@ specifiers open
Ice Cubes 2 dropped %@ open
Teable {{rowCount}} for {{count}} in 8 locales open
Medusa {{von}}/{{bis}} for {{from}}/{{until}} open
Strapi {tutaj}/{qui}/{bläddra} for {here}/{browse} in 7 locales open
Outline { appName }, {{documentTitle}}, {{username}} open
Linkwarden Spanish billed the annual price per month open

Scan results for all 23 in launch/upstream/FINDINGS.md; scripts/scan_upstream.sh owner/repo path runs it on any repo.


Then: translate what's missing

cd your-app
polygo init          # finds your string files, writes polygo.toml
polygo use gemma4    # pulls a model through Ollama, or openai/… with a key
polygo translate     # new and changed strings, into every target locale
polygo check         # the same checks, on what the model just wrote
git diff             # look it over, commit

🔒 Only the diff you meant
Writes into your existing files byte for byte. A lockfile remembers every source string, so changing one English string re-translates one string. Hand edits are never overwritten.

🧠 Context from your code
Before translating "Open" it finds Button("Open") in your source and tells the model it's a menu item, plus the closest strings you already translated. Blind-judged by a second model: 9 to 5.

🔢 Plurals per language
Polish gets one few many other, Japanese gets one form, each asked for with a concrete count. Written into .xcstrings variations, <plurals>, msgstr[n], i18next _few keys.

🚫 The model can't talk past check
Every reply is validated before it's written. Wrong twice and the string is quarantined as needs-review, not shipped. polygo review is a local page to approve or reject.

🧑‍⚖️ A second opinion
polygo audit has a different model grade each translation 1–5 with a reason. --fix redoes the flagged ones and leaves human edits alone.

🤖 Translate in CI
The same GitHub Action in mode: translate opens a PR with new translations and a coverage table on every push.


Formats

Format Used by What init finds
🍎 .xcstrings iOS, macOS one catalog with every locale; %#@var@ substitutions
🍎 .strings iOS, macOS (legacy) <locale>.lproj/Localizable.strings, UTF-16 kept as is
🤖 strings.xml Android res/values-*/; <plurals> per quantity, <string-array> per item
🌐 .json React, Vue, i18next locales/<locale>.json or locales/<locale>/<ns>.json, nested keys
🐦 .arb Flutter l10n.yaml + lib/l10n/app_<locale>.arb, ICU plurals
🐍 .po Django, Rails, PHP, gettext locale/<l>/LC_MESSAGES/*.po or flat; msgid_plural per Plural-Forms
🪟 .resx .resw .NET, WinUI Name.<locale>.resx, <locale>/Resources.resw

Every writer round-trips byte for byte, tested on 35 real files from DuckDuckGo, Ice Cubes, Grafana, Django, NewPipe and Penpot, so a translation never hides under a reformatting diff. No string files yet? polygo extract --rewrite pulls the text out of your web markup and swaps in t("key") calls (how).


Which model

Size Good for
qwen3:8b default 5 GB UI strings into major languages, on any laptop
gemma4 9.6 GB Smaller languages, Slavic plurals, anything a customer reads
openai/… anthropic/… API The same, without a local GPU

polygo models lists them, polygo use <model> switches. Honest numbers in docs/models.md.


Privacy

No telemetry, no account, no server. The only network request is the translation call to the provider in your polygo.toml; with Ollama that is 127.0.0.1. check, status, init and review never touch the network at all. Verify it yourself with sandbox-exec.


More

Commands and polygo.toml · Per-format notes · GitHub Action and pre-commit · Extracting strings from web apps · FAQ, compared with Lokalise / Crowdin / Weblate · How it was built

MIT · cargo test runs offline in about 10 s

About

Lokalise for one person. Translate your app's strings with a local model, keep the diff clean, check every placeholder and plural.

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages