No description
Find a file
Egor fureunoir Gorbunov 7f3070a334
Some checks failed
ci / rustfmt (push) Successful in 18s
ci / clippy (push) Failing after 20s
ci / test (push) Successful in 1m43s
youtra 0.1.1: re-translate when catalog's target lang differs from request
Bug: translating `en_GB.po` to `de` walked through the file, saw every
msgstr was non-empty, and reported "already translated" for all 236
entries. translated=0 for the user. Same hit anything where the file's
`Language:` header was unparseable (e.g. Django's free-form
"Language: BRITISH ENGLISH" — which `LanguageIdentifier::from_str`
silently accepts as a single 7-letter primary subtag, so the catalog
target happened to be neither None nor the requested target).

Two compounding root causes:

1. `translate_catalog` overwrote `catalog.target_lang =
   Some(target_lang.clone())` *before* deciding what to translate, so
   any "catalog target == requested target" check always saw equal.
2. `build_plans` always trusted `entry.is_translated()` regardless of
   whether the baked-in msgstr was in the language we now want.

Fix:

- `build_plans` now takes the requested target_lang and only honours
  pre-existing translations when `catalog.target_lang ==
  Some(target_lang)`. Otherwise (mismatch *or* unknown) those entries
  go to the provider.
- `translate_catalog` defers assigning the catalog target until after
  `build_plans` runs, so the original value is visible for that check.

Three unit tests cover all three branches (match → preserve,
mismatch → retranslate, unknown → retranslate).

Verified live on a Django `en_GB.po` with `Language: BRITISH ENGLISH`:
236 entries → 236 DeepL-translated to German, 0 placeholder mismatches,
output rewritten with the correct `Language: de` and `Plural-Forms:`
headers.

Only the `youtra` binary crate changes; libraries stay at 0.1.0 on
crates.io. Bumps youtra to 0.1.1 via an explicit `version = "0.1.1"`
override that supersedes the workspace-inherited 0.1.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 23:13:05 +03:00
.forgejo/workflows prep crates.io: rename cli to youtra, version pins, per-crate metadata 2026-05-28 18:07:58 +03:00
assets/logo youtra-core: initial workspace skeleton + core types 2026-05-27 23:24:27 +03:00
crates youtra 0.1.1: re-translate when catalog's target lang differs from request 2026-05-30 23:13:05 +03:00
tests/fixtures youtra-cli: MVP end-to-end pipeline with mock provider 2026-05-27 23:47:40 +03:00
.gitignore youtra 0.1.1: re-translate when catalog's target lang differs from request 2026-05-30 23:13:05 +03:00
Cargo.lock youtra 0.1.1: re-translate when catalog's target lang differs from request 2026-05-30 23:13:05 +03:00
Cargo.toml prep crates.io: rename cli to youtra, version pins, per-crate metadata 2026-05-28 18:07:58 +03:00
DECISIONS.md prep crates.io: rename cli to youtra, version pins, per-crate metadata 2026-05-28 18:07:58 +03:00
LICENSE prep crates.io: rename cli to youtra, version pins, per-crate metadata 2026-05-28 18:07:58 +03:00
README.md prep crates.io: rename cli to youtra, version pins, per-crate metadata 2026-05-28 18:07:58 +03:00

Youtra

Youtra

A Rust CLI that translates localization files (.po, .json) from one language to another using machine-translation APIs. You give it an API key, it figures out which provider the key belongs to.

Status: MVP feature-complete. Real providers (DeepL, Anthropic) and the mock provider work end-to-end. See DECISIONS.md for design notes; live API tests are gated behind env vars so cargo test is offline by default.

Quickstart

# Translate a gettext catalog from English to Russian using DeepL.
youtra en_GB.po -t ru_RU --api-key "$DEEPL_AUTH_KEY"

# JSON, i18next-style, into multiple targets at once. DeepL key shape is
# auto-detected; with -t ru,de,fr you get three files written next to the
# input (or into a directory passed via -o).
youtra locales/en.json -t ru,de,fr -o locales/ --api-key "$DEEPL_AUTH_KEY"

# Or use Claude as the translator.
youtra src.po -t ru --api-key "$ANTHROPIC_API_KEY"

# Offline / deterministic dev loop with the mock provider.
youtra src.po -t ru --api-key mock --no-cache

By default the SQLite cache lives at $XDG_CACHE_HOME/youtra/cache.db (%LOCALAPPDATA%\youtra\cache.db on Windows) so re-runs against the same (provider, source, target, source-string) hit the cache instead of the API. Override with --cache-dir or disable with --no-cache. Use --cache-only in CI to assert that nothing new needs translating.

Install

From crates.io (recommended once published):

cargo install youtra

Or build from source:

cargo install --path crates/youtra

Workspace

Crate Purpose
youtra-core Catalog, Entry, Format & Provider traits, errors, placeholder masking
youtra-formats .po (via polib) and .json adapters
youtra-providers DeepL, Anthropic, mock
youtra-cache SQLite-backed translation cache
youtra-plurals CLDR plural rules per target language
youtra The youtra binary (CLI)

Supported providers

Provider Auto-detected key shape Endpoint
DeepL Free UUID with :fx suffix api-free.deepl.com
DeepL Pro UUID without suffix api.deepl.com
Anthropic starts with sk-ant- api.anthropic.com
Mock literal mock none (testing only)

Force a specific provider with --provider deepl|anthropic|mock. The CLI errors out if multiple providers claim the same key shape (none currently do).

Placeholders that survive translation

Youtra masks placeholders before sending text to the provider and substitutes them back on return. The masker auto-picks a style from gettext format flags (c-format, python-format, python-brace-format); for JSON it protects {name} and {{name}} patterns by default. If a translated string is missing a placeholder or duplicates one, that entry is written out with the fuzzy flag so a human can review.

Plural handling

.po plurals (msgid_plural) and i18next-style _one / _other JSON keys are grouped into a single entry. On output the entry is fanned out to the CLDR form set the target language requires:

  • English / German / Spanish: [one, other]
  • Russian / Ukrainian: [one, few, many, other]
  • Arabic: [zero, one, two, few, many, other]
  • Japanese / Chinese / Korean: [other]

Synthesized forms get the fuzzy flag so a translator reviews them.

Contributing

Run cargo fmt, cargo clippy -- -D warnings, and cargo test before sending a patch. CI gates on the first two and on cargo test.

Live provider tests are #[ignore]d by default so plain cargo test is offline. Run them on demand with both the env var and --ignored:

YOUTRA_LIVE_DEEPL_KEY=... cargo test -p youtra-providers -- --ignored
YOUTRA_LIVE_ANTHROPIC_KEY=... cargo test -p youtra-providers -- --ignored

Heads up: DeepL returns HTTP 451 and Anthropic returns 403 from region-restricted networks even with valid keys, so these tests can fail for reasons unrelated to your changes.

License

Apache-2.0.