Skip to content

Translation for Developers

Overview

The ts() Function

All code with user-facing text must pass it through the ts() function, which takes the following arguments:

  1. A string literal (with no variables, line breaks, or concatenation).
  2. An optional array containing variables to substitute and/or a pluralized version of the string.

The function is available in PHP, Javascript, Smarty templates and Javascript templates:

PHP

  • For extensions, the ts function is provided by the civix-generated extensionUtil class and typically imported as E.

    use CRM_Myextension_ExtensionUtil as E;
    
    echo E::ts('Hello World!');
    
  • In CiviCRM core (non-extension) code, the import can be skipped and the global ts() function used directly:

    $options = [
      1 => ts('One'),
      2 => ts('Two'),
      3 => ts('Three'),
    ];
    
  • Since extensions are more common, the rest of these examples will be formatted with E::ts() assuming the use import is at the top of the file.

  • Example with variable substitution:

    $message = E::ts('A new %1 named "%2" has been created.', [
      1 => $itemType,
      2 => $itemTitle,
    ]);
    

    Note that variables should themselves be translated by your code before passing in, if appropriate.

  • If the string needs to be pluralized, use the singular form as the main string, and provide the count (integer) and plural (string) in the 2nd argument along with any placeholder values:

    $string = E::ts('1 item was created by %1', [
      'count' => $total,
      'plural' => '%count items were created by %1',
      1 => $userName,
    ]);
    

Javascript

  • The ts function is imported into a closure via the CRM.ts() function, which takes scope (extension name) and returns a function that always applies that scope to ts calls:
// This closure gets a local copy of jQuery, Lo-dash, and ts
(function($, _, ts) {

  let message = ts('Hello World!');

})(CRM.$, CRM._, CRM.ts('foo.bar.myextension'));
  • Variable substitutions and pluralization works the same as in php
let message = ts('1 item has been updated by %1.', {
  plural: '%count items have been updated by %1.',
  count: getCount(),
  1: getUserName(),
});

Note

CRM.ts is not the same as the global ts function. CRM.ts is a function that returns a function (javascript is wacky like that). Since your closure gives the local ts the same name as the global ts, it will be used instead.

Important

Your local variable must be named ts in order for the static code parser to extract translatable strings.

Javascript/HTML Templates

Any file ending in .html is assumed to use a javascript-based template language (e.g. Angular) and follow the same rules as javascript:

  • Angular Example
<div>
  <h2>{{ ts('1 Record', {plural: '%count Records', count: getCount()) }}</h2>
</div>
  • Angular Optimization

The curly-brace syntax supports an optimized one-time binding if the value never changes. For static strings with no variables, add :: after the opening braces for better performance:

<details>
  <summary>{{:: ts('Simple Title') }}</summary>
</details>

Smarty Templates

  • The strings hard-coded into templates should be wrapped in {ts}...{/ts} tags. For example:
<h2>
  {ts escape='html'}Hello World!{/ts}
</h2>
  • Variable substitution and pluralization follows the same rules:
<p>
  {ts escape='html' plural='%count items saved by %1' count=$count 1=$userName}1 item saved by %1{/ts}
</p>
  • Escaping rules are especially important in html attributes where the translation might contain a quote mark that would break the markup.
<a title="{ts escape='html'}Open "Date Picker"{/ts}">
  {ts escape='html'}Select Date{/ts}
</a>
  • Inline scripts use the 'js' escaping rule:
{literal}
<script type="text/javascript>
  CRM.$(function($) {
    let message = "{/literal}{ts escape='js'}Hello World{/ts}{literal}"; 
  });
</script>
{/literal}

Best practices

The general rules for avoiding errors may be summed up as:

  • The first argument to ts() must be a single, literal string.
  • In order for the static code parser to extract translatable strings, the first argument must not contain variables, concatenation, line-breaks, or leading/trailing spaces.
  • The second parameter of the ts() call is an array of variables to swap for placeholders in the string.
  • If a 'plural' is used, it must be passed into an array literal in the second argument (not a variable).

Use placeholders instead of variables inside strings

Bad

$string = E::ts("The date type '$name' has been saved.");

Good

$string = E::ts("The date type '%1' has been saved.", [1 => $name]);

Avoid tags inside strings

Bad

{ts}<p>Hello, world!</p>{/ts}

Good

<p>{ts escape='html'}Hello, world!{/ts}</p>

Hyperlinks within larger blocks of text are an exception to this rule, where you should place the <a> tags within the ts. Any link parameters should be provided as arguments to the ts. For example:

Bad

{ts}Here is a block of text with a link to the <a href="https://www.civicrm.org" target="_blank">CiviCRM Web Site</a>.{/ts}

OK

{ts 1='href="https://www.civicrm.org" target="_blank"'}Here is a block of text with a link to the <a %1>CiviCRM Web Site</a>.{/ts}

Smarty doesn't evaluate within single quotes, so if you are capturing an URL for a link, capture it with the href=" and optionally target="_blank".

OK

{capture assign=something}href="{crmURL p='civicrm/admin/something' q='reset=1'}" target="_blank"{/capture}
{ts 1=$something}Here is a block of text with a link to a <a %1>specific URL in CiviCRM</a>.{/ts}

For title attributes in <a> links, within CiviCRM these usually only appear in links that aren't within a larger block of text or where there is no clickable text, such as a datepicker icon. In this situation, the title text needs to be translated:

Bad

{ts}<a href="https://www.example.org/civicrm/something?reset=1" title="List participants for this event (all statuses)">Participants</a>{/ts}

Good

<a href="https://www.example.org/civicrm/something?reset=1" title="{ts escape='html'}List participants for this event (all statuses){/ts}">{ts}Participants{/ts}</a>

If there is no clickable text, use an accessible block:

Bad

<a title="Select Date"><i class="crm-i fa-calendar"></i></a>

Good

<a>
  <span class="sr-only>{ts escape='html'}Select Date{/ts}</span>
  <i class="crm-i fa-calendar" role="img" aria-hidden="true"></i>
</a>

Avoid multi-line strings

Even if your code editor may not like it, long strings should be on a single line since a change in indentation might change where the line breaks are, which would then require re-translating the string.

Bad

$string = ts("Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  Proin elementum, ex in pretium tincidunt, felis lorem facilisis 
  lacus, vel iaculis ex orci vitae risus. Maecenas in sapien ut velit
  scelerisque interdum.");

Good

$string = ts("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin elementum, ex in pretium tincidunt, felis lorem facilisis lacus, vel iaculis ex orci vitae risus. Maecenas in sapien ut velit scelerisque interdum.");

Avoid strings which begin or end with spaces

Bad

$string = $labelFormat['label'] . ts(' has been created.'),

Good

$string = ts('%1 has been created.', [1 => $labelFormat['label']]),

Avoid escaped quotes whenever possible

Bad

$string = ts('A new \'%1\' has been created.', [1 => $contactType]);

Good

$string = ts("A new '%1' has been created.", [1 => $contactType]);

Use separate strings for plural items

Bad

$string = ts('%1 item(s) were created by %2', [1 => $count, 2 => $userName]);

Good

$string = ts('%count item was created by %1', [
  'count' => $total,
  'plural' => '%count items were created by %1',
  1 => $userName,
]);

Ensure that strings have some words in them

Another common error is to use ts() to aggregate strings or as a "clever" way of writing shorter code:

Bad

Incorrect aggregation. This will be extremely confusing to translators and might give some really bad results in some languages.

$operation = empty($params['id']) ? ts('New') : ts('Edit'));
$string = ts("%1 %2", [1 => $operation, 2 => $contactType]);

OK

if (empty($params['id'])) {
  $string = ts("New %1", [1 => $contactType]);
}
else {
  $string = ts("Edit %1", [1 => $contactType]);
}

Note that this still makes it difficult to use the correct gender.

Include typography in strings

Typography is different in different languages and thus must be translated along with the string. For example, in French, there must be a space before a colon.

Bad

{ts}Event Total{/ts}:

Good

{ts}Event Total:{/ts}

Rationale for using Gettext

In most projects, strings are typically translated by either:

  • using Gettext (which is what CiviCRM does),
  • using arrays of key/string dictionaries,
  • using database lookups of strings (which is what Drupal does).

In order to be support Joomla!, WordPress, Backdrop and eventually other content management systems. Gettext is the standard way to translate strings in PHP, used by most projects.

For developers: Generate .po and .mo files for your extension

Sometimes you will want to generate the .po and .mo files for your extension during the development cycle, for testing (or for internal extensions, for production use).

Once your extension is stable you may want to consider publishing it on the Extensions Directory and getting it approved for in-app distribution you can then benefit from extension translation via Transifex where the Civi community can translate your extension and these translations can automatically be installed!

Here are the steps you'll need to follow:

Tools needed.

To generate the .po and .mo files you will need:

  1. A po editor examples include PoEdit and Virtaal
  2. The GetText utilities
  1. Download civistrings using the instructions in the readme.

  2. From the root directory of your extension do the following:

  3. Create an l10n directory: mkdir l10n

  4. Run CiviStrings civistrings -o "l10n/<Extension Shortname>.pot" . Note that the civistrings command format is:

    civistrings <OPTIONS> <PATH TO DIRECTORY CONTAINING EXTENSION>
    
  5. Once you have your pot file copy this into the expected directory structure, for example to translate your extension into French copy the pot file to <Extension Directory>/l10n/fr_FR/LC_MESSAGES/<Extension Shortname>.po (Replacing the parts denoted by <> with the relevant information and noting carefully the filename change from pot to po.)

  6. Use your po editor to translate the strings in the po file you copied.

  7. If your editor has not generated the corresponding mo binary file you can do this manually using the msgfmt command provided by GetText, as follows:

msgfmt <Extension Directory>/l10n/<Language>/LC_MESSAGES/<Extension Shortname>.po -o <Extension Directory>/l10n/<Language>/LC_MESSAGES/<Extension Shortname>.mo

Again, replacing the parts denoted by <> as appropriate.

Updating Strings

If you change your extension to add more E::ts or similar calls you will need to go through the same process of generating the pot file then copying it to a po file and editing that to then generate the mo file. Essentially start this process again!

Other guides/references

Here are the guides to other popular projects: