Language Resolution in Frappe Framework v15 (Python API)
What is language resolution in Frappe?
Language resolution in Frappe Framework v15 is the process of determining the active language for the current HTTP request and setting it on frappe.lang. This value controls which translations are used to render Desk, Website, and any translatable UI strings, server responses, or Jinja templates during that request.
Frappe computes frappe.lang per request, based on several possible inputs, evaluated in a specific priority order. Understanding this order is essential when you build multilingual ERPNext or custom Frappe apps.
How does Frappe v15 resolve frappe.lang?
Frappe resolves the active language in this priority order:
- Form Dict → _lang
- Cookie → preferred_language (guest users only)
- Request Header → Accept-Language (guest users only)
- User document → language
- System Settings → language
As soon as Frappe finds a valid language using one of these steps, it assigns it to frappe.lang and uses it for the rest of the request lifecycle.
How does _lang in form dict affect language?
What is frappe.form_dict._lang?
The Form Dict (frappe.form_dict) represents request parameters (query string, form data, etc.) in the current request. The _lang parameter in this dict has the highest priority in language resolution.
Key behavior: If _lang is present in frappe.form_dict, it updates all translatable components for that request, overriding any user or system settings.
Frappe itself uses this mechanism for features such as Email Templates and Print views, where you may want to render a document in a specific language independent of the user’s default language.
Example: forcing language per request
You can pass _lang via query string or POST data:
https://example.com/api/method/my_app.my_module.my_method?_lang=fr
Inside your method, Frappe has already resolved frappe.lang:
import frappe
@frappe.whitelist()
def my_method():
# Will be "fr" for this request if _lang=fr was passed
current_lang = frappe.lang
# your logic here
return {"language": current_lang}
This pattern is useful when:
- Rendering content for a specific locale (e.g., a PDF in German for a German customer).
- Testing translations quickly by switching _lang in the URL or form.
How does the preferred_language cookie work?
When is the preferred_language cookie used?
If _lang is not set in the form dict, Frappe checks the preferred_language cookie. This allows you to persist a language preference on the client (browser) without changing the user’s profile.
The documentation highlights two important constraints:
- The cookie is only considered for Guest Users.
- It is ignored for logged-in users.
This is typically used by the Website language switcher, where anonymous visitors can select a language that persists during their browsing session or until the cookie expires.
When should you use the cookie?
Use preferred_language when:
- You provide a language switcher on the public website (no login).
- You want a visitor’s language to persist across pages without attaching it to a user account.
- You don’t want to modify System Settings or User language for casual visitors.
Once a user logs in, the User document language becomes more important than the cookie, and the cookie value is ignored.
How does the Accept-Language header affect Frappe?
What is the Accept-Language header?
Accept-Language is a standard HTTP request header sent by browsers and clients to indicate preferred languages, such as:
Accept-Language: fr-CH,fr;q=0.9,en;q=0.8,de;q=0.7
If _lang and preferred_language are not set, Frappe resolves the language from this header for guest users.
Note: Like the cookie method, Accept-Language is only considered for guest users and is ignored for logged-in users.
Example: calling a Frappe API with language preference
curl -X GET "https://example.com/api/method/my_app.api.public_method" \
-H "Accept-Language: es"
In this call, if there is no _lang and no preferred_language cookie, Frappe will attempt to resolve frappe.lang to Spanish (“es”) based on the header.
This approach is ideal for:
- Headless integrations where the client controls locale via HTTP headers.
- Mobile apps or external systems that want localized responses without modifying user settings.
How do User and System Settings control language?
User document language field
For logged-in users, Frappe eventually falls back to the language field on the User document if no higher-priority source is set.
- This setting persists across devices and clients for the same user.
- It controls language for both Website and Desk views.
- A user can have their preferred language even if the site’s default language is different.
Example scenario from the reference: a user sets their language to Russian on a French site. When they log in, the site is translated to Russian automatically.
System Settings language field
If none of the previous mechanisms apply, Frappe uses the System Settings → language value. This is the fallback language for the entire site, and it has the lowest priority.
This ensures that:
- There is always a defined language for the site.
- New or anonymous sessions still see consistent language if no overrides are in place.
When should I use each language resolution mechanism?
1. _lang (Form Dict) – per-request override
Use _lang when you need a strict, per-request override:
- Generating a PDF or email in a specific language independent of user preferences.
- Providing a “View this document in language X” link.
- Running automated tests for translations by quickly switching request language.
2. preferred_language (cookie) – temporary guest preference
Use the cookie when you want guest users to have a persistent but lightweight preference:
- Website language switcher for visitors.
- Landing pages that remember the visitor’s last chosen language.
Avoid relying on the cookie for logged-in features, as it is ignored for authenticated users.
3. Accept-Language – respect client/browser default
Use the header as a sensible default for anonymous access:
- Public APIs consumed by localized clients.
- Websites where you want to auto-detect language from browser settings before the user interacts with a switcher.
4. User language – persistent user-level preference
Use the User document language field as the primary mechanism for logged-in users:
- Let each user select their preferred language in their profile.
- Ensure a consistent experience across Desk and Website for that user.
5. System Settings language – global fallback
Configure the System Settings language as the default site language:
- It defines the language for all sessions when no other signals are present.
- It should align with your primary business locale.
Practical examples for ERPNext and custom apps
Example 1: Multilingual public website
- Default site language: English (System Settings).
- Visitor’s browser is set to Spanish.
- Frappe uses Accept-Language: es to render Spanish by default for the guest.
- Visitor clicks a language switcher to switch to German.
- Site sets preferred_language=de cookie; future guest requests use German until changed.
Example 2: User-specific Desk language
- Company operates an ERPNext instance in English.
- A user prefers French, so they set User → language = “fr”.
- When logged in, all Desk and Website views appear in French, even if the site default is English.
- If they open a link with _lang=de, that specific request is rendered in German, but their user language remains French.
Example 3: Generating localized print formats
- A Sales Invoice needs to be printed in the customer’s language, not the user’s.
- Your custom print route or script can pass _lang to match the customer’s preferred language.
- Frappe resolves frappe.lang from _lang and prints all translatable strings accordingly.
Best practices for language resolution in Frappe v15
- Keep the resolution order in mind
Always remember the priority chain: _lang → preferred_language → Accept-Language → User.language → System Settings.language.
- Use user settings for long-term preferences
For authenticated workflows, rely primarily on the User’s language field instead of cookies or headers.
- Reserve _lang for explicit overrides
Do not overuse _lang globally; keep it for routes or actions that truly require forced language.
- Test guest vs logged-in behavior separately
Since cookies and Accept-Language are ignored for logged-in users, test both cases to avoid confusion.
- Ensure translations exist for active languages
Language resolution only selects the language; you must still maintain proper translation files (CSV/PO) for strings to appear localized.
Common issues and troubleshooting tips
Why isn’t my language changing for a logged-in user?
You may be relying on the preferred_language cookie or Accept-Language header.
- For logged-in users, Frappe ignores these and uses User.language, unless _lang is explicitly passed. Frappe Documentation
- Update the User’s language field or use _lang in the request for a one-off override.
Why does my guest user still see the old language?
- Check if _lang is present in the URL; it overrides cookie and header.
- Confirm that preferred_language cookie is being set correctly and not blocked by the browser.
- Verify that the requested language code is supported and configured in your site.
My API client sends Accept-Language but language does not change
- If your client is authenticated as a user, the header is ignored.
- For authenticated APIs, either update the User.language field or explicitly pass _lang as a parameter.
Target audience and prerequisites
Audience:
- Frappe and ERPNext developers building multilingual apps
- Implementers configuring language behavior for global ERPNext deployments
- Technical writers and integrators working with localized APIs
Prerequisites:
- Working knowledge of Frappe Framework v15
- Basic understanding of HTTP requests, headers, cookies
- Familiarity with User and System Settings in ERPNext/Frappe
Related topics and references
- Official Frappe Documentation: Language Resolution (Python API)
- Translations in Your Custom Frappe App – guide for managing translation files and languages.
- Frappe GitHub (v15 Branch): for implementation details around frappe.lang, request handling, and translations.