Metadata-Version: 2.4
Name: deepl-i18n
Version: 0.1.0
Summary: DeepL-first translation of JSON localization files, with placeholder-safety and a local-LLM fallback.
Author: Lorenz Schubert
License: MIT
Project-URL: Repository, https://github.com/lancedelgardo/deepl-i18n
Project-URL: Issues, https://github.com/lancedelgardo/deepl-i18n/issues
Keywords: i18n,l10n,translation,deepl,localization,json
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Localization
Classifier: Environment :: Console
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: deepl<2,>=1.16
Provides-Extra: yaml
Requires-Dist: pyyaml>=6; extra == "yaml"
Provides-Extra: mcp
Requires-Dist: mcp>=1.2; extra == "mcp"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pyyaml>=6; extra == "dev"
Dynamic: license-file

# deepl-i18n

[![CI](https://github.com/lancedelgardo/deepl-i18n/actions/workflows/ci.yml/badge.svg)](https://github.com/lancedelgardo/deepl-i18n/actions/workflows/ci.yml)

Translate your JSON localization files with the [DeepL API](https://www.deepl.com/pro-api).
DeepL-first, config-driven, and free — no telemetry, no account required beyond a
DeepL API key (the free tier gives 500,000 characters/month).

> **Unofficial project.** Not affiliated with, sponsored by, or endorsed by DeepL SE.
> "DeepL" is a trademark of DeepL SE; used here only to describe API compatibility.

Point it at a source-language JSON file, list your target languages, and it fills in
only the **missing** keys — existing translations are preserved, obsolete keys are
removed, and placeholders are protected from corruption.

> Optionally translate via a local LLM (LM Studio) instead of DeepL — see
> [Local model](#local-model-lm-studio).

## Features

- **Incremental** — only missing/empty keys are translated; existing values stay untouched.
- **Placeholder-safe** — variables like `{{ name }}` are shielded from translation and
  verified afterwards, catching both changed tokens and corruption where a placeholder
  gets glued to a word (e.g. `d{{value1}}ových`). The placeholder format is configurable
  (`{{ }}`, `%s`, `{0}`, …).
- **Multiple formats** — JSON and YAML (nested), Java `.properties` and iOS `.strings`
  (flat); auto-detected by extension or forced with `--file-format`. Nested objects are
  recursed; non-string values (numbers, booleans, null) are copied verbatim.
- **Formality, context & model** — set DeepL `formality`, a `context` prompt, and the
  `model_type` (quality- vs. latency-optimized) to steer tone, terminology and speed.
- **Cost transparency** — every run reports the characters DeepL actually billed.
- **Dry run** — see how many keys and characters would be translated, and whether it fits
  the DeepL free tier, without spending any quota.
- **Quota display** — real-time DeepL character usage.

## Requirements

- Python 3.13+
- A DeepL API key
- `deepl` (the only third-party dependency)

## Setup

```bash
# create & activate a virtual environment
python -m venv venv
./venv/Scripts/activate        # Windows
# source venv/bin/activate     # macOS/Linux

# install the CLI (and its only dependency, deepl) from source
pip install -e .
```

This installs a `deepl-i18n` command. `config.json` and `.env` are resolved from the
**current working directory**, so run the command from your project folder (or pass
`--config path/to/config.json`).

### API key

Provide your DeepL API key in one of these ways (checked in this order):

1. `DEEPL_API_KEY` environment variable (recommended, especially for CI)
2. a `.env` file in the project root containing `DEEPL_API_KEY=your-key`
   (copy `.env.example` to `.env`)

`.env` and `config.json` are git-ignored so your key and local paths never get committed.

### Configuration

Copy the example config and adjust it:

```bash
cp config.example.json config.json
```

```json
{
  "source_lang": "EN",
  "output_languages": ["DE", "FR", "ES", "IT", "PT-PT"],
  "filename_overrides": {},
  "formality": "default",
  "placeholder_pattern": "{{\\s*\\w+\\s*}}",
  "context": "Optional: describe your app so DeepL picks the right terminology and tone.",
  "targets": {
    "app": {
      "input_path": "./locales/en.json",
      "output_dir": "./locales"
    }
  }
}
```

| Key | Meaning |
|---|---|
| `source_lang` | DeepL source language code of your master file (e.g. `EN`, `DE`). |
| `output_languages` | Target language codes (e.g. `DE`, `FR`, `PT-PT`, `EN-US`). |
| `filename_overrides` | Map a language code to a custom output filename, e.g. `{"EN-US": "en"}` → `en.json`. |
| `formality` | `less`, `more`, `prefer_less`, `prefer_more`, or `default` (omit). Applied only to languages that support it. |
| `placeholder_pattern` | Regex matching your placeholders. Default `{{ … }}`. Use e.g. `%\\d*\\$?[sd@]` (printf) or `\\{\\d+\\}` (MessageFormat). |
| `context` | Optional prompt passed to DeepL to disambiguate terminology and tone. |
| `glossary` | Optional path to a glossary file (see [Glossary](#glossary)), or `null`. |
| `file_format` | Force a format (`json`/`yaml`/`properties`/`strings`), or `null` to auto-detect by extension. |
| `model_type` | DeepL model: `quality_optimized` (default), `latency_optimized`, `prefer_quality_optimized`, or `null`. |
| `targets` | Named input/output path pairs. Each needs `input_path` (the source file) and `output_dir`. |

Output filenames use the lowercase language code (e.g. `de.json`, `pt-pt.json`) unless
overridden.

## Usage

All commands take a `<target>` — a key under `targets` in your `config.json`.

```bash
# translate the missing keys of a target
deepl-i18n translate app

# only one language
deepl-i18n translate app --lang FR

# dry run — no API calls, no files written; shows keys, characters and free-tier fit
deepl-i18n translate app --dry-run
deepl-i18n translate app --dry-run --lang FR

# re-translate existing keys, and keep a .bak of each file before overwriting
deepl-i18n translate app --override --backup

# use a config elsewhere
deepl-i18n translate app --config ./locales/config.json
```

### Watch

Re-translate automatically whenever the source file changes:

```bash
deepl-i18n watch app                 # polls the source file (default every 2s)
deepl-i18n watch app --interval 5 --backup
```

### Check (CI gate)

Validate existing translations without translating anything: reports missing/empty keys
and placeholder corruption, and **exits non-zero** if any problem is found — ready to drop
into CI.

```bash
deepl-i18n check app
deepl-i18n check app --lang FR
```

### DeepL quota

```bash
deepl-i18n usage
```

Also printed automatically at the end of every translate run and dry run.

## File formats

The format is detected from the source file's extension; output files use the same
extension. Force it with `--file-format` (or `file_format` in the config) when the
extension is ambiguous or missing.

| Format | Extensions | Structure | Notes |
|---|---|---|---|
| JSON | `.json` | nested | i18next-style; non-string values preserved |
| YAML | `.yaml`, `.yml` | nested | needs `pip install deepl-i18n[yaml]`; comments not preserved |
| Java properties | `.properties` | flat | `#`/`!` comments, `=`/`:` separators, line continuations |
| iOS strings | `.strings` | flat | `/* */` and `//` comments |
| Flutter ARB | `.arb` | flat | `@@locale` and `@key` metadata are preserved, never translated |
| Android XML | `.xml` | flat | `<string>` elements; respects `translatable="false"` |
| CSV | `.csv` | flat | `key,value` columns; optional header row |

```bash
deepl-i18n translate app --file-format yaml
```

More formats (gettext `.po`, ARB template regeneration, Android `<string-array>`/`<plurals>`)
are on the roadmap.

## Glossary

Keep terminology consistent across languages. Point `glossary` in your config (or pass
`--glossary path`) at a JSON file with two optional sections:

```json
{
  "doNotTranslate": ["BeerpongMe", "Rerack", "Bracket"],
  "terms": {
    "Turnier": { "FR": "tournoi", "ES": "torneo" }
  }
}
```

- **`doNotTranslate`** — terms kept verbatim in *every* target language.
- **`terms`** — per-term, per-language forced translations (language codes match your
  `output_languages`; a base code like `EN` also matches `EN-US`).

For **DeepL**, an ephemeral glossary is created for each target language, used for that
run, and deleted afterwards (glossary create/delete cost no characters). Language pairs
DeepL doesn't support for glossaries are skipped with a note. For the **local model**, the
same glossary is injected into the prompt.

```bash
# validate a glossary file and preview entries per language (no API calls)
deepl-i18n glossary check app --glossary glossary.json

# manage glossaries stored on DeepL
deepl-i18n glossary list
deepl-i18n glossary delete <id>
deepl-i18n glossary delete --all
```

## Local model (LM Studio)

Instead of DeepL you can translate with a local LLM through LM Studio's OpenAI-compatible
server (default `http://localhost:1234`, default model `openai/gpt-oss-20b`). Same
key-selection logic (missing keys only), same output files. Batch mode groups several keys
per request; keys the batch returns broken (e.g. corrupted placeholders) are retried
individually.

```bash
deepl-i18n local app                 # translate target "app" (batch mode)
deepl-i18n local app --lang CS
deepl-i18n local app --dry-run
deepl-i18n local app --model "openai/gpt-oss-20b"
deepl-i18n local app --single        # key-by-key instead of batch
deepl-i18n local app --chunk-size 20 # keys per batch request (default 30)
```

Compare a local model against existing (DeepL) translations without writing anything:

```bash
deepl-i18n compare app --lang CS
```

`LMSTUDIO_MODEL` and `LMSTUDIO_URL` can be set via environment variables.

## MCP server

Expose the tool to MCP clients (Claude Desktop, Claude Code, …) so an agent can estimate,
check and run translations for you.

```bash
pip install deepl-i18n[mcp]
```

Tools:

| Tool | What it does |
|---|---|
| `dry_run_estimate` | missing keys/characters per language, free-tier fit — no API calls |
| `check_integrity` | completeness + placeholder check; returns `{ok, issues}` |
| `deepl_usage` | current DeepL quota |
| `translate_missing` | translate missing keys and write the files (uses quota) |

Each tool takes a `target` (a key under `targets`), an optional `lang`, and an optional
`config` path. Register the server with your client (set `cwd` to the folder that holds
your `config.json` and API key):

```json
{
  "mcpServers": {
    "deepl-i18n": {
      "command": "deepl-i18n-mcp",
      "cwd": "/path/to/your/i18n/project"
    }
  }
}
```

## Development

```bash
pip install -e .[dev]   # installs pytest + pyyaml
pytest -q               # runs the offline test suite (no DeepL calls)
```

CI runs the suite on Python 3.10–3.13 via GitHub Actions (`.github/workflows/ci.yml`).

## Roadmap

This project is being generalized from an internal tool into a public, DeepL-first i18n
CLI. See [ROADMAP.md](ROADMAP.md) for planned work (unified CLI, `pip` install, glossary
support, more providers, an MCP server, more file formats, and more).

## License

[MIT](LICENSE) © 2026 Lorenz Schubert — free to use, modify and distribute, including
commercially, as long as the copyright notice is kept.
