Skip to main content

Translate Apple String Catalogs and Fastlane App Store metadata.

Project description

App Localizer

Test

Translate Xcode String Catalogs (.xcstrings) and Fastlane App Store metadata from the command line with any OpenAI-compatible chat completions endpoint.

About

App Localizer is a development and CI tool for app repositories. It edits localization inputs in place, records what it generated, and skips unchanged work on later runs.

The output is a translation draft. It still belongs in code review, especially UI, legal text, purchase flows, and App Store copy. Do not ship App Localizer in an app target, call it from app code, or put provider API keys in mobile apps.

Supported Inputs

Two input types are supported, configured independently:

  • String Catalogs. Fills missing or untranslated .xcstrings entries. Preserves placeholders, substitutions, variation trees, translator comments, and localizations outside the current run.
  • Fastlane metadata. Translates fastlane/metadata/<locale>/*.txt App Store copy, copies URL fields verbatim, and enforces App Store character limits.

Quick Start

Installation

Install the CLI from PyPI:

uv tool install app-localizer

pipx install app-localizer and python3 -m pip install app-localizer work too. Pin a version for reproducible installs:

uv tool install "app-localizer==1.0.0"

Configuration

Add app-localizer.toml to the app repository root with the sections the repository needs:

[xcstrings]
sources = ["App/Resources/Localizable.xcstrings"]
languages = ["de", "es", "fr", "ja"]

[fastlane]
metadata_path = "fastlane/metadata"
source_locale = "en-US"
locales = ["de-DE", "es-ES", "fr-FR", "ja"]

Paths resolve relative to the config file, so the same config works from CI and local shells when --config points at it.

First Run

Validate the config and file structure without a provider API key:

app-localizer --check

Preview pending translation work without writing files:

app-localizer --dry-run

Translate configured inputs:

export OPENAI_API_KEY=...
app-localizer

A run rewrites catalogs and metadata in place, then updates .app-localizer.lock files. Commit the config, generated localization files, metadata, and lockfiles. Do not commit API keys or app-localizer.secrets.toml.

Command-Line Interface

Common Commands

Run these from the app repository root unless passing --config:

# Validate config, catalogs, metadata, and existing generated files.
app-localizer --check

# Show pending work without network access or file writes.
app-localizer --dry-run

# Translate every configured domain.
app-localizer

# Process one configured domain.
app-localizer --domain fastlane

# Retranslate even values already marked translated or locked.
app-localizer --force

--check and --dry-run do not require an API key. --fail-on-untranslated makes either command exit nonzero while work is pending. It is additive: a run can enable the policy, but cannot disable fail_on_untranslated = true set in the config.

Options

--config selects a config file. Without it, the CLI reads app-localizer.toml from the working directory when that file exists.

Provider overrides are run-specific: --base-url, --model, --reasoning-effort, --temperature, and --candidates. --api-key supplies a provider key directly and wins over environment variables and the secrets file. --secrets selects a different TOML secrets file.

--verbose enables debug logging. --version prints the installed package version. app-localizer --help prints the full parser output.

Ad Hoc Catalog Runs

For one-off String Catalog translation without a config file, pass catalog paths and target languages directly:

app-localizer --source Localizable.xcstrings --languages de es fr

Ad hoc runs configure only the String Catalog domain. Use a config file for repeatable app-team workflows.

Configuration Reference

All non-secret settings live in app-localizer.toml. Unknown keys are rejected, so typos fail early. See examples/app-localizer.toml for a copyable template.

Provider Settings

Provider settings are top-level and shared by both domains:

Key Default Description
base_url https://api.openai.com/v1 OpenAI-compatible chat completions endpoint.
api_key_env OPENAI_API_KEY Environment variable holding the API key. Set to "" for keyless local endpoints.
model gpt-5.2 Model identifier to request from the endpoint.
reasoning_effort unset Optional reasoning effort: none, minimal, low, medium, high, or xhigh. Unsupported endpoints are retried without it for the rest of the run.
temperature 0.2 Sampling temperature for translation requests.
candidates 3 Candidate translations requested per value before the decider pass chooses one.
fail_on_untranslated false Makes --check and --dry-run fail while anything is pending.

String Catalogs

[xcstrings] enables .xcstrings translation:

Key Description
sources Catalog paths, resolved relative to the config file.
languages Target Xcode language tags, for example de, pt-BR, zh-Hans.

Fastlane Metadata

[fastlane] enables App Store metadata translation:

Key Description
metadata_path Fastlane metadata directory, resolved relative to the config file.
source_locale Locale directory to translate from, for example en-US. Fastlane's default fallback directory also works.
locales Target App Store Connect locales, for example de-DE, fr-FR. Typos like de fail validation.
keywords_trim_to_fit Defaults to true. Generated keywords.txt translations are normalized, deduplicated case-insensitively, and trimmed from the tail until the comma-joined list fits 100 characters. Set to false to reject over-limit keyword translations instead.
allow_unknown_locales Defaults to false. Set to true to accept App Store locales added after this package's release instead of failing validation. Unknown locales warn; malformed tags still fail.

Secrets

API keys do not belong in app-localizer.toml. Resolution order is --api-key, then the environment variable named by api_key_env, then the secrets file. --secrets selects a different file.

Use an environment variable for individual developer machines:

export OPENAI_API_KEY=...
app-localizer

Teams can share app-localizer.secrets.toml beside the config file. Each entry maps an environment variable name to its value; one file can hold keys for every provider a team uses:

OPENAI_API_KEY = "sk-..."
OPENROUTER_API_KEY = "sk-or-..."

Add the secrets file to the consuming repository's .gitignore and distribute it out of band. See docs/ios-integration.md for the full key-handling policy.

Provider Examples

Any OpenAI-compatible chat completions endpoint works. The default targets OpenAI and reads OPENAI_API_KEY.

OpenRouter:

base_url = "https://openrouter.ai/api/v1"
api_key_env = "OPENROUTER_API_KEY"
model = "anthropic/claude-sonnet-4-6"

Ollama:

base_url = "http://localhost:11434/v1"
api_key_env = ""
model = "llama3.3"

LM Studio:

base_url = "http://localhost:1234/v1"
api_key_env = ""
model = "qwen2.5-32b-instruct"

Apple Foundation Models has no direct HTTP API. Point base_url at a local OpenAI-compatible bridge and set api_key_env = "" if the bridge is keyless.

Leave reasoning_effort unset unless the selected model and endpoint accept it. When an endpoint reports the field as unknown or unsupported, the run warns once, retries without it, and omits it for the rest of that run.

Outputs

String Catalog Behavior

For each configured catalog and target language, a run:

  • Adds missing target-language localizations from the source language.
  • Skips entries marked with shouldTranslate: false.
  • Translates stringUnit values, nested variation trees, and substitution variations (%#@name@ / %arg).
  • Adds the plural categories the target language's CLDR rules require, even when the source language omits them.
  • Preserves substitution metadata such as argNum and formatSpecifier, and keeps it in sync with the source language.
  • Preserves localizations for languages outside the current run.
  • Copies whitespace-only values verbatim instead of sending them to the provider.

Catalog writes are atomic. The writer keeps file permissions, syncs contents to disk, sorts JSON keys, and uses Xcode-style spacing before replacing the original file. Catalogs with duplicate JSON keys or unsupported catalog versions are rejected.

Fastlane Metadata Behavior

Files in the source locale directory are handled by name:

File Handling Limit
name.txt translated 30
subtitle.txt translated 30
description.txt translated 4000
keywords.txt translated, kept comma-separated 100
promotional_text.txt translated 170
release_notes.txt translated 4000
apple_tv_privacy_policy.txt translated -
marketing_url.txt, support_url.txt, privacy_url.txt copied verbatim, never sent to the provider -
review_information/ ignored -

Non-localized metadata at the root of metadata_path (copyright.txt, category files, and review_information/) lives outside the locale directories and is never touched.

Unrecognized files are skipped with a warning. For prose metadata files, a translation over the App Store character limit is rejected like a placeholder mismatch. If no candidate fits, the file errors and the run fails.

Generated keywords.txt translations differ when keywords_trim_to_fit = true: spaces around keywords are stripped, case-insensitive duplicates are removed, and trailing keywords are dropped until the comma-joined list fits 100 characters. Dropped terms are logged in a warning. Set keywords_trim_to_fit = false to reject over-limit keyword translations instead. --check still fails when a source file exceeds its own limit.

Metadata .txt files have no per-value translation state, so skip decisions come from the lockfile's per-file source hashes. --check validates locked target files against App Store character limits. Other hand-edited target changes with an unchanged source are not detected as pending. Use --force to rewrite everything.

Lockfiles and Skip Behavior

.app-localizer.lock is the skip cache. The String Catalog domain writes one beside each catalog. The Fastlane domain writes one at the metadata root.

Commit lockfiles. Without them, later runs have less information and translate more than they need to.

String Catalog lockfile section:

{
  "catalogs" : {
    "Localizable.xcstrings" : {
      "de" : {
        "baseUrl" : "https://api.openai.com/v1",
        "config" : "sha256...",
        "keys" : {
          "Localizable.xcstrings::welcome" : "sha256...",
          "files.count" : "sha256..."
        },
        "model" : "gpt-5.2",
        "timestamp" : "2026-06-11T00:00:00+00:00"
      }
    }
  },
  "generatedBy" : "app-localizer",
  "version" : 2
}

Fastlane lockfile section:

{
  "fastlane" : {
    "de-DE" : {
      "marketing_url.txt" : { "config" : "verbatim", "hash" : "sha256...", "..." : "..." },
      "name.txt" : {
        "baseUrl" : "https://api.openai.com/v1",
        "config" : "sha256...",
        "hash" : "sha256...",
        "model" : "gpt-5.2",
        "timestamp" : "2026-06-11T00:00:00+00:00"
      }
    }
  },
  "generatedBy" : "app-localizer",
  "version" : 2
}

For catalogs, keys maps each translated catalog key to a fingerprint of its source content. A key's units are translated again when that key's source changed, the configuration changed, or the target is missing or invalid. config is per language, so a configuration change re-translates every key for that language.

For metadata, hash fingerprints each source file. Skips are per file: a file is translated again when the target is missing, the source changed, or the configuration changed.

config fingerprints the endpoint, model, reasoning effort, temperature, candidate count, prompts, and domain-specific validation policy. For metadata, it also includes the character limit and keyword trim policy. baseUrl and model are informational fields that record what produced an entry.

Unreadable, unsupported, or foreign lockfiles are discarded with a warning and rebuilt on the next run. Sections a domain does not own are preserved.

Limits and Review

Translations are validated before writing:

  • Catalog translations must preserve printf placeholders, Swift interpolation placeholders, and String Catalog substitution tokens.
  • Fastlane metadata translations must fit App Store character limits.
  • Config files must not contain unknown keys, duplicate sources or languages, malformed language tags, or invalid App Store locale codes.

Current limits:

  • The catalog lockfile tracks source freshness per top-level key per language. Editing one source string re-translates only that key's units; per-unit translation state still lives in the catalog itself.
  • Plural variations are expanded to the categories the target language's CLDR rules require (Russian few and many, for example), translated from the source's other form. Languages outside the built-in CLDR table fall back to mirroring the source's categories.
  • Output is a draft. Require human review for high-risk UI, marketing copy, legal text, purchase flows, and short ambiguous strings.

Team Workflow

CI

Copy examples/github-actions/app-localizer.yml into the app repository. Pull requests run --check with no API key. Manual workflow dispatch runs translation from a repository secret and opens a draft PR with the generated changes.

The workflow installs App Localizer from PyPI. Pin a version in the install step for reproducible CI runs.

For release branches, set fail_on_untranslated = true or pass --fail-on-untranslated so checks fail while configured targets still have untranslated content.

App-Team Workflow

See docs/ios-integration.md for the recommended app-team workflow: API key handling, developer checks, CI translation PRs, review policy, and release-branch behavior.

Run translation before fastlane deliver so uploaded metadata is current.

Development

Local Development

Install the package editable and run the test suite from the repository root:

python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -e .
PYTHONPATH=src python3 -m unittest discover -s tests

CI runs the same suite on every push to main and every pull request against the oldest and newest supported Python versions.

Releasing

Releases are tag-driven: pushing a version tag builds the package and publishes it to PyPI.

  1. Update version in pyproject.toml and commit to main.
  2. Tag the same version with a leading v.
  3. Push main and the tag.
git tag vX.Y.Z
git push origin main vX.Y.Z

Pushing the tag runs the release workflow. It verifies the tag matches the package version, runs the test suite, builds the sdist and wheel, publishes to PyPI via trusted publishing, and publishes a GitHub Release with generated notes and the build files attached.

Consumers install a pinned release from PyPI:

python3 -m pip install "app-localizer==X.Y.Z"

Compatibility

Versioning follows semantic versioning as of 1.0.0. The stable surface is the CLI flag set, the app-localizer.toml config schema, and the .app-localizer.lock format. A major release is required to break any of them.

The default model (gpt-5.2) is not part of the stable surface and may change in a minor release; pin model in the config to control it. Translation output is model-defined and never guaranteed to be stable. The lockfile is a disposable cache: an unreadable or future-version lockfile is discarded and rebuilt, so its format can advance without a breaking change.

See CHANGELOG.md for release history.

Contributions

App Localizer is not accepting source contributions at this time. Bug reports will be considered.

Author

Kyle Hughes

Bluesky
LinkedIn
Mastodon

License

App Localizer is available under the MIT license.

See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

app_localizer-1.0.0.tar.gz (66.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

app_localizer-1.0.0-py3-none-any.whl (40.7 kB view details)

Uploaded Python 3

File details

Details for the file app_localizer-1.0.0.tar.gz.

File metadata

  • Download URL: app_localizer-1.0.0.tar.gz
  • Upload date:
  • Size: 66.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for app_localizer-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d958f32088c6e3a28c9033dc3123e471b413bea7873379da49d39c28fba5f3ce
MD5 ef71567a3c150ca36d0e793e3b2ff127
BLAKE2b-256 78abf1f3fb9583dad9b7d714d96caf7a7220f0e6fdbbb59f827d70a34e978d5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for app_localizer-1.0.0.tar.gz:

Publisher: release.yml on kylehughes/app-localizer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file app_localizer-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: app_localizer-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 40.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for app_localizer-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c708ff12b1e2d16d9ec7f29927802babc8525f0af202ba9ab050e5e27a7503eb
MD5 50a556047165da826129ec2bae395b9c
BLAKE2b-256 4806c7120dd46bf7b10758bd08af5c08d67844d32084ca3f16b80da1b48dbdbf

See more details on using hashes here.

Provenance

The following attestation bundles were made for app_localizer-1.0.0-py3-none-any.whl:

Publisher: release.yml on kylehughes/app-localizer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page