Frappe comes with a built-in translation system for capturing translatable strings from code and extracting them into CSV files for different languages.

How it works

Translations happen in three steps.

1. Extracting translatable strings

Many strings like field label and description that are easily parseable from JSON files of DocTypes are automatically marked as translatable without any explicit hint.

Other strings are extracted from source code files such as .json, .js, .py. These are extracted only if they are marked explicitly by the author in _() or __() methods.

For example:

message = _("You don't have permissions to access this file")

2. Translating the extracted strings

Once we have a list of strings that we want to translate, they are put into CSV files based on the language. Translations are primarily contributed by various users of Frappe and ERPNext on the translator portal.

The submitted translations are verified and then converted into a Pull Request by a bot every week and merged into the core.

3. Translating strings in app

Translations are nothing but a key-value pair made of original string and it's translation in a language. It might look like this:

{
    "1 hour ago": "एक घंटे पहले",
    "1 minute ago": "1 मिनट पहले",
    "1 month": "1 महीना",
    "1 week": "1 सप्ताह",
    "1 year": "1 साल",
    "3 months": "3 महीने",
    "6 months": "6 महीने",
    "About": "के बारे में",
    "About Us Settings": "हमारे बारे में सेटिंग्स",
    "About Us Team Member": "हमारे बारे में टीम के सदस्य",
    "Academic Term": "शैक्षणिक शब्द",
    "Academic Year": "शैक्षणिक वर्ष",
    "Academics User": "अकादमिक उपयोगकर्ता",
    ...
}

When the string is written as __('1 hour ago') in the code, the __ method looks up the dictionary to find whether a translation exists for that string. If yes, the translated string would be returned and it would show up.

This was an example of how translations work in JavaScript files, but the same works in Python too.

Tips on writing a valid translatable string

There are a few rules that need to be followed for the translation parser to pick up the strings properly from code.

1. Literal strings

The string to be translated must always be a literal string, not a variable or expression.

Example in Python

# This will work
message = _('Document submitted successfully')
frappe.msgprint(message)

# This will also work
frappe.msgprint(_('Document submitted successfully'))

# This won't work
message = 'Document submitted successfully'
frappe.msgprint(_(message))

Example in JavaScript

// This will work
message = __('Document submitted successfully')
frappe.msgprint(message)

// This won't work
message = 'Document submitted successfully'
frappe.msgprint(__(message))

2. Variables

If you have variables in your string, you must use the positional formatter {0}, any other type of formatter won't work.

Example in Python

# This is fine
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)

# These are not
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)

_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})

# This one uses the positional formatter,
# but won't work because the string is formatted before it is passed to _()
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))

Example in JavaScript

// This is fine
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name])

// This is not
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`)

3. Blocks

Don't split your string into separate blocks of strings and then concatenate them. Don't write multiline strings. Always write your string in a single even if the string is very large.

Example in Python

# This is fine
_('You have {0} subscribers in your mailing list.').format(len(subscribers))

# Don't split strings
_('You have ') + len(subscribers) + _(' subscribers in your mailing list.')

# Don't write multiline strings
_('You have {0} subscribers \
    in your mailing list').format(len(subscribers))

Example in JavaScript

// This is fine
__('You have {0} subscribers in your mailing list.', [subscribers.length])

// Don't split strings
__('You have ') + subscribers.length + __(' subscribers in your mailing list.')

// Don't write multiline strings
__('You have {0} subscribers' +
    'in your mailing list', [subscribers.length])

4. Plural

Don't try to pluralize words using logic. Every language has different plural forms.

# Don't do this
msg = _("You have {0} pending invoice").format(invoice_count)
if invoice_count > 1:
  msg += _("s")

# Write separate strings
# Every language has different plural forms
if invoice_count > 1:
  msg = _("You have {0} pending invoices").format(invoice_count)
else:
  msg = _("You have {0} pending invoice").format(invoice_count)

5. No Trailing Spaces

Don't start or end the sentence with spaces. Trailing spaces gets trimmed for other languages when passed through translation engine.

If you have to add space around your text, you can do it outside the translation syntax.

# Don't do this
msg = _(" You have {0} pending invoice ")

# Do this
msg = ' ' + _("You have {0} pending invoices") + ' '

6. Adding context for a string

A translatable string can have different meaning in different context. For example, string "Change" can mean "to make or become different" or "Coins". So to tackle this issue context variable can be used to set the context for a string so that it can be translated differently in different language using Translation Tool.

In JavaScript


__("Change", null, "Coins")

// Here "Coins" is the context for text "Change"

In Python


_("Change", context="Switch")

# Here "Switch" is the context for text "Change"

Adding a New Language

To add a new language, follow these steps:

Step 1: Export to a file

$ bench --site sitename get-untranslated [lang] [path-to-file]

Step 2: Translate

Create another file with updated translations (in the same order as the source file). For this you can use the Google Translator or Bing Translator.

Step 3: Import your translations

$ bench update-translations [lang] [source-path] [translated-path]

A new file will be created with the name [lang].csv in the translations folder in each app.

Step 4: Update languages.json

Add your language in frappe/geo/languages.json

Step 5: Commit each app and push

Commit your changes with the .csv files in each app and push them to their repositories.