Translations
Language support and how to contribute translations.
Sofa supports multiple languages for the web and mobile apps. The current supported locales are:
- English (source)
- Arabic
- Chinese
- Dutch
- French
- German
- Hebrew
- Italian
- Japanese
- Korean
- Portuguese
- Spanish
You can switch languages in Settings > Language on web or the mobile app. Sofa detects your browser or device language on first launch and defaults to it if supported. This setting is not saved to the server — it is only persisted to the current browser or device.
How translations work
Sofa uses Lingui for internationalization. All translatable strings are extracted from the source code into PO files (an industry-standard format), which live in packages/i18n/src/po/.
The translation pipeline:
- React (web) and React Native (mobile) apps wrap UI strings in
<Trans>components ormsg`...`macros bun run i18n:extractscans the codebase and updatesen.powith new strings- Translations are added to each locale's
.pofile (e.g.fr.po,de.po) - The Vite and Metro plugins compile
.pofiles on the fly at dev/build time - Non-English locales are lazy-loaded on demand
Contributing via Crowdin
The easiest way to contribute translations is through Crowdin, a community translation platform. No development setup required — just sign up and start translating in the browser!
When source strings change on main, they are automatically uploaded to Crowdin via GitHub Actions. To pull completed translations back into the repo, a maintainer triggers the download workflow which creates a PR with the updated .po files.
AI translation with Claude
For maintainers, Sofa includes a script, claude.ts, that uses Claude Code to roughly translate any yet-to-be-translated strings.
# Translate a single locale
bun packages/i18n/scripts/claude.ts fr
# Translate all locales (runs in parallel)
bun packages/i18n/scripts/claude.ts all
# Preview without translating
bun packages/i18n/scripts/claude.ts all --dry-run
# Use a specific model
bun packages/i18n/scripts/claude.ts all --model opusThe script only translates strings with empty msgstr — existing translations are never overwritten. It reads the locale list from lingui.config.ts automatically.
Using this script really only makes sense if you have a Claude Max subscription with Anthropic's cash to burn.
Adding a new language
- Add the locale code to the
localesarray inlingui.config.ts - Run
bun run i18n:extract— this creates a new.pofile for the locale - Translate the strings (via Crowdin, manually, or with
bun packages/i18n/scripts/claude.ts <locale>) - Add the locale to the
LOCALE_INFOarray inpackages/i18n/src/locales.tswith its native name
Translation guidelines
Whether translating via Crowdin, AI, or manually, these rules apply:
- Keep it concise — these are UI labels, buttons, and short messages
- Preserve placeholders —
{0},{name},{count, plural, ...},<0>text</0>must appear in the translation exactly as in the source - Adapt plurals — ICU plural blocks should use the correct plural categories for your language (e.g. French needs
one/other, Czech needsone/few/many/other) - Don't translate brand names — Sofa, TMDB, Plex, Jellyfin, etc. stay as-is