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, bench get-untranslated, RTL, localization.4license: MIT5---6
7# Frappe Translation / i18n
8
9> Deterministic patterns for translating Frappe apps across v14, v15, and v16.
10
11---
12
13## Quick Reference
14
15| 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()` |
22
23---
24
25## Decision Tree
26
27```
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 lazy
33├── 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 _() needed
41
42Where do translations live?
43├── v14 → apps/{app}/{app}/translations/{lang}.csv
44├── v15+ → apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po
45└── User overrides → Translation DocType (highest priority)
46
47Need to extract untranslated strings?
48├── v14 → bench --site {site} get-untranslated {lang} {output}
49└── v15+ → bench generate-pot-file --app {app}
50```
51
52---
53
54## Translation Priority (Highest First)
55
56| 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 |
62
63---
64
65## Version Differences
66
67| 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 |
76
77---
78
79## Auto-Extracted Strings (No _() Needed)
80
81These are extracted automatically by the framework:
82
83- 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**
90
91---
92
93## String Extraction Rules
94
95| 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 |
102
103**CRITICAL**: Extractors work on the AST/tokens. They CANNOT extract dynamically constructed strings. See [Anti-Patterns](references/anti-patterns.md).
104
105---
106
107## Anti-Patterns (NEVER Do These)
108
109| 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")` |
118
119> Full anti-pattern catalog with code examples: [references/anti-patterns.md](references/anti-patterns.md)
120
121---
122
123## CSV Translation File Format
124
125**Location**: `apps/{app}/{app}/translations/{lang}.csv`
126
127```csv
128"source","translation","context"
129"Hello","Hallo",""
130"Change","Wisselgeld","Coins"
131"Change","Wijziging","Amendment"
132```
133
134- ALWAYS use UTF-8 encoding (no BOM)
135- ALWAYS quote all fields with double quotes
136- Context column is optional but MUST be present (empty string if unused)
137- No hooks registration needed — auto-discovered from `translations/` directory
138
139---
140
141## PO/MO Files [v15+]
142
143**Location**: `apps/{app}/{app}/locale/{lang}/LC_MESSAGES/{app}.po`
144
145```bash
146# Generate POT template
147bench generate-pot-file --app {app}
148
149# Migrate existing CSV to PO
150bench migrate-csv-to-po --app {app}
151
152# Compile PO to MO (required for runtime)
153bench compile-po-to-mo --app {app}
154```
155
156PO files follow standard GNU gettext format. Use any PO editor (Poedit, Weblate, Transifex).
157
158---
159
160## Bench Commands
161
162| 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 |
169
170---
171
172## RTL Support
173
174**Hardcoded RTL languages**: `ar` (Arabic), `he` (Hebrew), `fa` (Persian/Farsi), `ps` (Pashto)
175
176```python
177# Python
178if frappe.utils.is_rtl():
179 # Apply RTL-specific logic
180```
181
182```javascript
183// JavaScript
184if (frappe.utils.is_rtl()) {
185 // Apply RTL-specific logic
186}
187```
188
189- Frappe auto-applies `dir="rtl"` to the `<html>` element
190- ALWAYS use logical CSS properties (`margin-inline-start` not `margin-left`) for RTL compatibility
191- Bootstrap RTL stylesheet is auto-loaded when RTL language is active
192
193---
194
195## Custom App Translation Workflow
196
197### Adding translations to your custom app:
198
1991. **Write translatable strings** using `_()` / `__()` with positional placeholders
2002. **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`
209
210---
211
212## Reference Files
213
214| 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 |