| name | ollama_intl |
| description | Translates i18n resource files (JSON, YAML) using Ollama and TranslateGemma. Supports Simple, FormatJS/React-Intl, Crowdin/extracted, and Rails formats. Detects format automatically and round-trips each format (output matches input). Preserves ICU MessageFormat placeholders. Use when translating locale files, i18n resources, or when the user mentions translating JSON/YAML language files with Ollama. |
| license | MIT |
| compatibility | Requires a running Ollama instance with translategemma:27b pulled, or any OpenAI-compatible LLM endpoint. Requires Rust toolchain to build. |
| metadata | {"author":"Kieran","version":"0.1.0"} |
| allowed-tools | Bash(cargo:*) Bash(ollama:*) Bash(ollama_intl:*) Read |
ollama-intl
Translates i18n resource files using Ollama and TranslateGemma. Supports four formats, auto-detected by content and file extension:
- Simple — flat
{ "key": "value" } maps
- FormatJS — React-Intl compiled output with
defaultMessage keys
- Crowdin / extracted —
@formatjs/cli extract output with message keys
- Rails — Ruby i18n YAML with locale wrapper
Install via cargo install --git https://github.com/v0l/ollama_intl.git. Written in Rust.
Quick start
Prerequisites
Make sure Ollama is running and the model is pulled:
ollama pull translategemma:27b
Install
cargo install --git https://github.com/v0l/ollama_intl.git
This places ollama_intl in your Cargo bin directory (typically ~/.cargo/bin/, which should be on $PATH).
To build locally instead:
cargo build --release
Usage
Usage: ollama_intl [OPTIONS] --target <TARGET>...
Options:
-u, --url <URL> Ollama base URL (must include /v1) [default: http://localhost:11434/v1]
-m, --model <MODEL> Model name [default: translategemma:27b]
--source <SOURCE> Source language as "Name:code" [default: English:en]
-t, --target <TARGET> Target language(s) as "Name:code", repeatable
-o, --output-dir <DIR> Directory to write output files; required
-i, --input <INPUT> Input file path, or "-" for stdin [default: -]
-h, --help Print help
Examples
Translate to a single language:
ollama_intl -i src/locales/en.json -o src/locales -t German:de
Translate to multiple languages in parallel (each with its own progress bar):
ollama_intl \
-i src/locales/en.json \
-o src/locales \
-t German:de -t Spanish:es -t French:fr \
-t Portuguese:pt -t Japanese:ja -t Chinese:zh
Translate a Rails YAML file:
ollama_intl -i config/locales/en.yml -o config/locales -t French:fr
Pipe from stdin:
echo '{"greeting": "Hello"}' | ollama_intl -t German:de
Use a remote Ollama instance:
ollama_intl -u http://10.100.2.32:11434/v1 -i en.json -t Spanish:es -o .
Retranslate everything, ignoring existing translations:
ollama_intl -i en.json -o . -t German:de --force
Supported formats
| Format | Description | Example |
|---|
| Simple | Flat key→string map | { "key": "value" } / key: value |
| FormatJS | React-Intl compiled output | { "id": { "defaultMessage": "...", "description": "..." } } |
| Crowdin | @formatjs/cli extract output | { "hash": { "message": "..." } } |
| Rails | Ruby i18n YAML with locale wrapper | en:\n key: value |
Each format is round-tripped — output matches input format. Crowdin files stay Crowdin, FormatJS stays FormatJS, etc.
Features
ICU MessageFormat support
The tool parses ICU MessageFormat (the standard behind react-intl/FormatJS) and translates each Literal segment individually, preserving argument names, plural keywords, #, select branches, and all non-literal tokens exactly as-is.
Placeholder verification
After translation, all placeholders from the source are verified to still be present in the translated text. If any are missing, the source text is kept unchanged and a warning is printed.
Generic plural detection
Strings using patterns like {# {unit}s} (where a runtime argument is mixed with a literal suffix inside a plural) are detected and warned about, since static translation cannot be accurate without knowing the runtime value.
Incremental translation
By default, existing translation files are loaded and only missing keys are translated. Existing translations are preserved. Pass --force to retranslate everything.
Key removal
Keys present in existing translations but no longer in the source file are removed from the output.
Build and run during development
cargo run --release -- -i en.json -o output/ -t German:de
For just a debug build (faster compile, slower runtime):
cargo run -- -i en.json -o output/ -t German:de
Running tests
cargo test
cargo test -- --ignored
Project structure
ollama_intl/
├── SKILL.md # This file
├── Cargo.toml # Rust dependencies
├── README.md # Full documentation
└── src/
├── main.rs # CLI arg parsing, orchestration, parallel spawning
├── parse.rs # JSON/YAML parsing, format detection, serialisation
├── translate.rs # LLM client, ICU parsing, translation, placeholder verification
├── types.rs # IntlFile enum, FormattedMessage struct
└── translategemma.txt # LLM prompt template
Modifying the code
- CLI args: edit the
Args struct in src/main.rs
- Adding a new format: add a variant to
IntlFile in src/types.rs, implement parsing in src/parse.rs, serialisation in src/parse.rs::serialise
- Changing the LLM prompt: edit
src/translategemma.txt
- Changing placeholder validation: edit
check_placeholders in src/translate.rs
Edge cases and gotchas
- Crowdin format: Values must have exactly a
"message" string field. The output preserves the same {"hash": {"message": "..."}} structure.
- Rails YAML: The top-level locale key is preserved as-is in the output — update it manually if the target language code differs.
- FormatJS descriptions: The
description field is preserved verbatim (not translated) since it's a developer hint, not user-visible text.
- Escaped braces:
{{ and }} are correctly treated as literal braces, not placeholders.
- Time unit abbreviations: The prompt contains special instructions to translate "hr", "min", "sec", etc. as time units not general vocabulary.