How to translate your custom app? A Step-by-Step Guide

How to Add and Manage Translations in Your Custom Frappe App: A Step-by-Step Guide


1. Create the translations Folder

Step:
In your custom app directory, create a folder named translations at:

apps/your-app/your-app/translations/

Why?
Frappe automatically detects this folder. It follows a convention-over-configuration approach, so placing your translation files here ensures they’re recognized without extra setup.


2. Add Translation CSV Files

Step:
Create a CSV file for each language in the translations folder. For example:

  • ar.csv (Arabic)
  • es.csv (Spanish)

CSV Structure:

"source_string","translated_string","context"
"bl7","بلح",""
"toto","توتو",""

Why?

  • Column 1 (source_string): The original string used in your code (e.g., _("toto")).
  • Column 2 (translated_string): The translated version of the string.
  • Column 3 (context): Optional. Use it to clarify the translation context.

The third column is optional and may be left empty.


3. Use Translatable Strings in Your Code

Step:
Wrap strings in your Python, HTML, or JavaScript code using _():

from frappe import _

def my_function():
    message = _("toto")  # Will be replaced with "توتو" for Arabic users

Why?
The _() function marks strings as translatable. During runtime, Frappe replaces them based on the user’s language preference.


4. Build the Site

Only needed if you’re using __() in custom frontend JS and changed the JS files

Step:
Run the build command:

bench --site your-site build

5. Verify Translations

Step:

  • Switch your user’s language to Arabic (or target language) in Frappe.
  • Check if _("toto") now shows as "توتو".

Why?
This confirms that Frappe correctly maps the source string to its translated counterpart.


Key Notes

Language Codes

  • CSV filenames must match Frappe’s language code (e.g., ar for Arabic).
  • You can find these codes in Frappe > Language settings.

Folder Location

  • The translations folder must be at the root of your app’s package directory (not inside a submodule).

Performance

  • Translations are cached during build.
  • Always rebuild after editing translation files.

Fallback Behavior

  • If a string isn’t translated, Frappe will use the original (source) string.

Do i need to build or migrate the site?

NO, frappe loads translations dynamically at runtime from your translations/lang.csv file, based on the logged-in user’s language preference.
you just need to refresh your browser or clear-cashe if necessary.

bench build only needed if you’re using __() in custom frontend JS and changed the JS files


Example Workflow

  1. Create:

    apps/my_app/my_app/translations/ar.csv
    
  2. Add entries:

    "welcome_message","مرحبا بك!",""
    
  3. Use in code:

    _("welcome_message")
    
  4. Clear Cashe:

  • use it if translations don’t reflect immediately
    bench --site my-site clear-cache
    
  1. Build:
  • only needed if you’re using __() in custom frontend JS and changed the JS files
    bench --site my-site build
    
  1. Test in Arabic interface.

Explain Context?

context is nothing but a meta-data,
to explain to the one who will use this translation when and where to use it.
because sometimes you have the same word with different meaning

as example:

"charge","شحن",""
"charge","رسوم",""

here we have same word but different meaning (different context to use)
so, to filter it we have to give it a context

"charge","شحن","mobile phone charge"
"charge","رسوم","invoice line"

in code:

_("charge") # here we don't know which meaning (context to use)
_("charge", context="mobile phone charge") # here we mean شحن
_("charge", context="invoice line") # here we mean رسوم
8 Likes

Can you explain the context?

context is nothing but a meta-data,
to explain to the one who will use this translation when and where to use it.
because sometimes you have the same word with different meaning

as example:

"charge","شحن",""
"charge","رسوم",""

here we have same word but different meaning (different context to use)
so, to filter it we have to give it a context

"charge","شحن","mobile phone charge"
"charge","رسوم","invoice line"

in code:

_("charge") # here we don't know which meaning (context to use)
_("charge", context="mobile phone charge") # here we mean شحن
_("charge", context="invoice line") # here we mean رسوم

will frappe code care for this and use it in the correct context?

i didn’t use context infact and never needs it so idk
but you can see this for better understanding:

Got it, and I don’t think it’s implemented fully

I tested this using current ERPNext text, like menu or doctype, after bench build, clear-cache, migrate, stop and start the dev server, it won’t change the standard text. I can only use Translation document to translate it. What steps am I missing or is it only for _() code?

1- each language has it’s own id (language code)
as example :
arabic => ar
english => en
you can find it in ‘Language’ doctype

so, make sure that the csv file name match the language code

2- in your python code you must use
from frappe import _
_(“your word in the file”)


3- make sure that you are using string not a variable
as example:
x = “hi”
_(x) => that is wrong

_(“hi”) => this is the right way

hence my question, can I use, for instance, Purchase Order or Employee in the desk, I tested it’s not working. Or not?

i think there is a missing step,
i will find it in my custom app then update it in the article

that’s odd
sorry i was talking about custom-apps,
but it’s the same with erpnext
:thinking:

After translating in translations/lang.csv, what happens if a user adds a different meaning for the same word in the Translation doctype?

Which translation will the system use?

if you are talking about the (translation doctype)
then i think the system will give respect to the doctype over the translations/lang.csv

i didn’t try it but lets see…

@mohamed-ameer

I just tested it. It work as you said.
The system give respect to the doctype over the translations/lang.csv.

I think this is good for us, which user can add their own translation if some word meaning does not correct as they expect.

1 Like

Dear @mohamed-ameer

This is example in my custom app, on python file
frappe.throw(_("Cannot create invoice for order {}.").format(link))

Translate to TH
"Cannot create invoice for order {}.","ไม่สามารถสร้างใบแจ้งหนี้สำหรับคำสั่งซื้อ {}.",""

After change language to Thai(TH), it not translate to thai.
Does this not support for translation, right?

I was trying to correct the hu.csv language file in the ERPNext app.
The whole bench with the site and apps is running in Docker using docker compose.

I copied hu.csv from the ERPNext app’s translation folder in the backend container, made corrections, uploaded it back, and then ran the clear-cache and build commands in the backend container.

However, as a result, the styles broke, and now I only see plain HTML.

The updated translation was applied, so the changes worked, but the styling issue appeared.

The docker compose restart does not fixed the issue. Only docker compose down and then up fix it, but the translation dissapear, so the files are restored.

see this,

1 Like

Hi
I followed this post to install ERPNext and Frappe CRM on Synology, and it works:Running Frappe + HRMS and many more apps via Portainer on Synology NAS (finally working!) - Install / Update - Frappe Forum

I found no where to set Frappe CRM local language(only English), So I followed your guide try to translate the UI.

But after installation ,there were 11 docker containers, I am quite new to Linux and Docker, I found

4 container had “apps“ path

bash-4.4$ sudo docker exec -it ERPNext-BACKEND ls
apps config env logs patches.txt sites

bash-4.4$ sudo docker exec -it ERPNext-FRONTEND ls
apps config env logs patches.txt sites

bash-4.4$ sudo docker exec -it ERPNext-WEBOSCKET ls
apps config env logs patches.txt sites

bash-4.4$ sudo docker exec -it ERPNext-QUEUE-DEFAULT ls
apps config env logs patches.txt sites

I just want to localize the Frappe-CRM UI, which path should I put csv file in.

Thanks very much.

1 Like