Frappe Translation / i18n
Deterministic patterns for translating Frappe apps across v14, v15, and v16.
Quick Reference
| Task |
Python |
JavaScript |
| Translate string |
_("Hello") |
__("Hello") |
| With substitution |
_("Hello {0}").format(name) |
__("Hello {0}", [name]) |
| With context |
_("Change", context="Coins") |
__("Change", null, "Coins") |
| Lazy (module-level) |
_lt("Pending") [v15+] |
N/A |
| Check RTL |
frappe.utils.is_rtl() |
frappe.utils.is_rtl() |
Decision Tree
Need to translate a string?
├── In Python (.py)?
│ ├── Inside a function/method → _("text {0}").format(val)
│ ├── Module-level constant [v15+] → _lt("text")
│ └── Module-level constant [v14] → define inside function or use lazy
├── In JavaScript (.js)?
│ └── ALWAYS → __("text {0}", [val])
├── In Jinja template (.html)?
│ └── {{ _("text") }}
├── In Vue (.vue)?
│ └── __("text") in <script>, {{ __("text") }} in <template>
└── DocType label/description/option?
└── Auto-extracted — no _() needed
Where do translations live?
├── v14 → apps/{app}/{app}/translations/{lang}.csv
├── v15+ → apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po
└── User overrides → Translation DocType (highest priority)
Need to extract untranslated strings?
├── v14 → bench --site {site} get-untranslated {lang} {output}
└── v15+ → bench generate-pot-file --app {app}
Translation Priority (Highest First)
| Priority |
Source |
Scope |
| 1 |
Translation DocType (user overrides) |
Per-site |
| 2 |
MO files (locale/{lang}/.../{app}.mo) |
Per-app [v15+] |
| 3 |
CSV files (translations/{lang}.csv) |
Per-app |
| 4 |
Parent language (e.g., pt for pt-BR) |
Fallback |
Version Differences
| Feature |
v14 |
v15 |
v16 |
_() / __() |
Yes |
Yes |
Yes |
_lt() lazy translation |
No |
Yes |
Yes |
| CSV translations |
Yes |
Yes (legacy) |
Yes (legacy) |
| PO/MO (gettext) |
No |
Yes |
Yes |
bench generate-pot-file |
No |
Yes |
Yes |
| Babel JS extractor |
No |
Yes |
Yes |
Type hints on _() |
No |
No |
Yes |
Auto-Extracted Strings (No _() Needed)
These are extracted automatically by the framework:
- DocType labels and descriptions
- Select field options (each option line)
- Workflow states and actions
- Print Format labels
- Report column labels
- Notification subjects (not body)
- Dashboard chart labels
String Extraction Rules
| File Type |
Extractor |
What It Finds |
.py |
Babel (AST) |
_("..."), _lt("...") calls |
.js |
Babel tokenizer [v15+] / regex [v14] |
__("...") calls |
.html |
Regex |
{{ _("...") }} in Jinja |
.vue |
Same as JS |
__("...") in script/template |
.json |
DocType parser |
Labels, descriptions, options |
CRITICAL: Extractors work on the AST/tokens. They CANNOT extract dynamically constructed strings. See Anti-Patterns.
Anti-Patterns (NEVER Do These)
| Pattern |
Why It Breaks |
Correct Form |
_(f"Hello {name}") |
f-string not extractable |
_("Hello {0}").format(name) |
_("Hello " + name) |
Concatenation fragments |
_("Hello {0}").format(name) |
_("Welcome %s") % name |
Old-style not extractable |
_("Welcome {0}").format(name) |
__(`Hello ${name}`) |
Template literal not extractable |
__("Hello {0}", [name]) |
_(" Hello ") |
Leading/trailing spaces trimmed |
_("Hello") |
_("item" if x else "items") |
Ternary inside _() |
_("item") if x else _("items") |
_(variable) |
Variable not extractable |
_("Known String") |
Full anti-pattern catalog with code examples: references/anti-patterns.md
CSV Translation File Format
Location: apps/{app}/{app}/translations/{lang}.csv
"source","translation","context"
"Hello","Hallo",""
"Change","Wisselgeld","Coins"
"Change","Wijziging","Amendment"
- ALWAYS use UTF-8 encoding (no BOM)
- ALWAYS quote all fields with double quotes
- Context column is optional but MUST be present (empty string if unused)
- No hooks registration needed — auto-discovered from
translations/ directory
PO/MO Files [v15+]
Location: apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po
# Generate POT template
bench generate-pot-file --app {app}
# Migrate existing CSV to PO
bench migrate-csv-to-po --app {app}
# Compile PO to MO (required for runtime)
bench compile-po-to-mo --app {app}
PO files follow standard GNU gettext format. Use any PO editor (Poedit, Weblate, Transifex).
Bench Commands
| Command |
Version |
Purpose |
bench --site {site} get-untranslated {lang} {output.csv} |
All |
Export untranslated strings |
bench update-translations {lang} {untranslated.csv} {translated.csv} |
All |
Import translations |
bench generate-pot-file --app {app} |
v15+ |
Generate .pot template |
bench migrate-csv-to-po --app {app} |
v15+ |
Convert CSV to PO format |
bench compile-po-to-mo --app {app} |
v15+ |
Compile PO to binary MO |
RTL Support
Hardcoded RTL languages: ar (Arabic), he (Hebrew), fa (Persian/Farsi), ps (Pashto)
# Python
if frappe.utils.is_rtl():
# Apply RTL-specific logic
// JavaScript
if (frappe.utils.is_rtl()) {
// Apply RTL-specific logic
}
- Frappe auto-applies
dir="rtl" to the <html> element
- ALWAYS use logical CSS properties (
margin-inline-start not margin-left) for RTL compatibility
- Bootstrap RTL stylesheet is auto-loaded when RTL language is active
Custom App Translation Workflow
Adding translations to your custom app:
- Write translatable strings using
_() / __() with positional placeholders
- Extract untranslated strings:
- v14:
bench --site {site} get-untranslated {lang} untranslated.csv
- v15+:
bench generate-pot-file --app {app}
- Translate the extracted strings (manually or via PO editor)
- Place translations:
- CSV:
apps/{app}/{app}/translations/{lang}.csv
- PO:
apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po
- Compile (v15+ PO only):
bench compile-po-to-mo --app {app}
- Clear cache:
bench --site {site} clear-cache
Reference Files
| File |
Contents |
| references/api-reference.md |
Full Python _() and JS __() API with all signatures and edge cases |
| references/csv-and-bench.md |
CSV format spec, bench commands, PO/MO workflow, custom app setup |
| references/anti-patterns.md |
Complete anti-pattern catalog with failing and corrected examples |
1---2name: frappe-core-translation3description: Use when implementing translations/i18n in Frappe v14-v16 apps. Covers _() in Python, __() in JavaScript, CSV translation files, bench commands, string extraction rules, lazy translation _lt(), PO/MO files [v15+], RTL support, and custom app translations. Prevents common mistakes with f-strings, concatenation, and template literals that break string extraction. Keywords: translation, i18n, _(), __(), _lt(), CSV, PO, gettext,, translate my app, multi-language, text not translated, wrong language, how to add translation. bench get-untranslated, RTL, localization.4license: MIT5---67# Frappe Translation / i18n89> Deterministic patterns for translating Frappe apps across v14, v15, and v16.1011---1213## Quick Reference1415| Task | Python | JavaScript |16|------|--------|------------|17| Translate string | `_("Hello")` | `__("Hello")` |18| With substitution | `_("Hello {0}").format(name)` | `__("Hello {0}", [name])` |19| With context | `_("Change", context="Coins")` | `__("Change", null, "Coins")` |20| Lazy (module-level) | `_lt("Pending")` [v15+] | N/A |21| Check RTL | `frappe.utils.is_rtl()` | `frappe.utils.is_rtl()` |2223---2425## Decision Tree2627```28Need to translate a string?29├── In Python (.py)?30│ ├── Inside a function/method → _("text {0}").format(val)31│ ├── Module-level constant [v15+] → _lt("text")32│ └── Module-level constant [v14] → define inside function or use lazy33├── In JavaScript (.js)?34│ └── ALWAYS → __("text {0}", [val])35├── In Jinja template (.html)?36│ └── {{ _("text") }}37├── In Vue (.vue)?38│ └── __("text") in <script>, {{ __("text") }} in <template>39└── DocType label/description/option?40 └── Auto-extracted — no _() needed4142Where do translations live?43├── v14 → apps/{app}/{app}/translations/{lang}.csv44├── v15+ → apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po45└── User overrides → Translation DocType (highest priority)4647Need to extract untranslated strings?48├── v14 → bench --site {site} get-untranslated {lang} {output}49└── v15+ → bench generate-pot-file --app {app}50```5152---5354## Translation Priority (Highest First)5556| Priority | Source | Scope |57|----------|--------|-------|58| 1 | **Translation DocType** (user overrides) | Per-site |59| 2 | **MO files** (`locale/{lang}/.../{app}.mo`) | Per-app [v15+] |60| 3 | **CSV files** (`translations/{lang}.csv`) | Per-app |61| 4 | **Parent language** (e.g., `pt` for `pt-BR`) | Fallback |6263---6465## Version Differences6667| Feature | v14 | v15 | v16 |68|---------|-----|-----|-----|69| `_()` / `__()` | Yes | Yes | Yes |70| `_lt()` lazy translation | No | Yes | Yes |71| CSV translations | Yes | Yes (legacy) | Yes (legacy) |72| PO/MO (gettext) | No | Yes | Yes |73| `bench generate-pot-file` | No | Yes | Yes |74| Babel JS extractor | No | Yes | Yes |75| Type hints on `_()` | No | No | Yes |7677---7879## Auto-Extracted Strings (No _() Needed)8081These are extracted automatically by the framework:8283- DocType **labels** and **descriptions**84- Select field **options** (each option line)85- Workflow **states** and **actions**86- Print Format **labels**87- Report **column labels**88- Notification **subjects** (not body)89- Dashboard chart **labels**9091---9293## String Extraction Rules9495| File Type | Extractor | What It Finds |96|-----------|-----------|---------------|97| `.py` | Babel (AST) | `_("...")`, `_lt("...")` calls |98| `.js` | Babel tokenizer [v15+] / regex [v14] | `__("...")` calls |99| `.html` | Regex | `{{ _("...") }}` in Jinja |100| `.vue` | Same as JS | `__("...")` in script/template |101| `.json` | DocType parser | Labels, descriptions, options |102103**CRITICAL**: Extractors work on the AST/tokens. They CANNOT extract dynamically constructed strings. See [Anti-Patterns](references/anti-patterns.md).104105---106107## Anti-Patterns (NEVER Do These)108109| Pattern | Why It Breaks | Correct Form |110|---------|---------------|--------------|111| `_(f"Hello {name}")` | f-string not extractable | `_("Hello {0}").format(name)` |112| `_("Hello " + name)` | Concatenation fragments | `_("Hello {0}").format(name)` |113| `_("Welcome %s") % name` | Old-style not extractable | `_("Welcome {0}").format(name)` |114| `` __(`Hello ${name}`) `` | Template literal not extractable | `__("Hello {0}", [name])` |115| `_(" Hello ")` | Leading/trailing spaces trimmed | `_("Hello")` |116| `_("item" if x else "items")` | Ternary inside _() | `_("item") if x else _("items")` |117| `_(variable)` | Variable not extractable | `_("Known String")` |118119> Full anti-pattern catalog with code examples: [references/anti-patterns.md](references/anti-patterns.md)120121---122123## CSV Translation File Format124125**Location**: `apps/{app}/{app}/translations/{lang}.csv`126127```csv128"source","translation","context"129"Hello","Hallo",""130"Change","Wisselgeld","Coins"131"Change","Wijziging","Amendment"132```133134- ALWAYS use UTF-8 encoding (no BOM)135- ALWAYS quote all fields with double quotes136- Context column is optional but MUST be present (empty string if unused)137- No hooks registration needed — auto-discovered from `translations/` directory138139---140141## PO/MO Files [v15+]142143**Location**: `apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po`144145```bash146# Generate POT template147bench generate-pot-file --app {app}148149# Migrate existing CSV to PO150bench migrate-csv-to-po --app {app}151152# Compile PO to MO (required for runtime)153bench compile-po-to-mo --app {app}154```155156PO files follow standard GNU gettext format. Use any PO editor (Poedit, Weblate, Transifex).157158---159160## Bench Commands161162| Command | Version | Purpose |163|---------|---------|---------|164| `bench --site {site} get-untranslated {lang} {output.csv}` | All | Export untranslated strings |165| `bench update-translations {lang} {untranslated.csv} {translated.csv}` | All | Import translations |166| `bench generate-pot-file --app {app}` | v15+ | Generate .pot template |167| `bench migrate-csv-to-po --app {app}` | v15+ | Convert CSV to PO format |168| `bench compile-po-to-mo --app {app}` | v15+ | Compile PO to binary MO |169170---171172## RTL Support173174**Hardcoded RTL languages**: `ar` (Arabic), `he` (Hebrew), `fa` (Persian/Farsi), `ps` (Pashto)175176```python177# Python178if frappe.utils.is_rtl():179 # Apply RTL-specific logic180```181182```javascript183// JavaScript184if (frappe.utils.is_rtl()) {185 // Apply RTL-specific logic186}187```188189- Frappe auto-applies `dir="rtl"` to the `<html>` element190- ALWAYS use logical CSS properties (`margin-inline-start` not `margin-left`) for RTL compatibility191- Bootstrap RTL stylesheet is auto-loaded when RTL language is active192193---194195## Custom App Translation Workflow196197### Adding translations to your custom app:1981991. **Write translatable strings** using `_()` / `__()` with positional placeholders2002. **Extract untranslated strings**:201 - v14: `bench --site {site} get-untranslated {lang} untranslated.csv`202 - v15+: `bench generate-pot-file --app {app}`2033. **Translate** the extracted strings (manually or via PO editor)2044. **Place translations**:205 - CSV: `apps/{app}/{app}/translations/{lang}.csv`206 - PO: `apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po`2075. **Compile** (v15+ PO only): `bench compile-po-to-mo --app {app}`2086. **Clear cache**: `bench --site {site} clear-cache`209210---211212## Reference Files213214| File | Contents |215|------|----------|216| [references/api-reference.md](references/api-reference.md) | Full Python _() and JS __() API with all signatures and edge cases |217| [references/csv-and-bench.md](references/csv-and-bench.md) | CSV format spec, bench commands, PO/MO workflow, custom app setup |218| [references/anti-patterns.md](references/anti-patterns.md) | Complete anti-pattern catalog with failing and corrected examples |