Localization

The t() function

Introduction

When talking about localization, t() is the main function that developers should be aware of.

It accepts one or more parameters. The first parameter is the string that should be translated. So, for instance, for a Cancel button you should write:

<button><?php echo t('Cancel'); ?></button>

Concrete CMS will automatically translate Cancel to the current user language. Great, isn't it?

Parameters

Often, you have to insert a dynamic content into a translatable string.

Let's do an example: if $blockTypeName contains a block type name and you want to translate the following code:

echo 'Edit '.$blockTypeName;

You may think to write it as echo t('Edit '.$blockTypeName). This is wrong, since the translatable string must be a constant. You may also think to write echo t('Edit').' '.$blockTypeName;. This approach too should be avoided, since it supposes that in every language the translation of 'Edit' comes before the block type name (for instance, in German they are reversed). So, the correct way to write it is echo sprintf(t('Edit %s'), $blockTypeName);. t is smart enough to simplify your life, integrating the sprintf php function; so you can write the above line as:

echo t('Edit %s', $blockTypeName);

What if we've more than one variable to insert into the string to be translated? In this case too we have to be quite careful, since words order can be different from one language to another.

For instance, if you need to enable translatation for this:

echo 'Posted by '.$user.' on '.$date;

Instead of writing echo t('Posted by %s on %s', $user, $date); you should write:

echo t('Posted by %1$s on %2$s', $user, $date);

Using numbered arguments will allow translators to switch the arguments order to allow more correct translations.

PS: To know the format of the variable placeholders see the description of the sprintf parameters in the PHP manual.

Comments

Especially when using more parameters, it's often useful to communicate to the translators the meaning of the various variable placeholders (the %s).

You can do this with a simple php comment starting with i18n, like in this example:

echo t(/*i18n %1$s is a user name, %2$s is a date/time*/'Posted by %1$s at %2$s', $user, $date);

Those parameters will be visible to translators, giving them a great help.

The t2() function

You may want to translate strings that are number-dependent, and that require different translations for singular and plural forms. One common mistake is to do a simple php if-then-else, like this:

echo ($pages == 1) ? t('%d page', $pages) : t('%d pages', $pages);

This supposes that the plural forms in all languages are only two, and that the singular form is when $page is one and the plural form is when $page is not one. This assumption is wrong. For instance, Russian has three plural forms, and in French zero is singular (we have zéro page and not zéro pages).

So, when you want to translate plural forms, don't use t(): there's its brother t2(). It takes at least three parameters, as in this example:

echo t2('%d page', '%d pages', $pages);

The t2() function returns the correct plural forms associated to the number stored in the third parameter (the $pages variable in this case). Furthermore, a call to sprintf is called, so that $pages will be inserted in the resulting string.

The tc() function

Sometimes, an English text may have multiple meanings, so it may have multiple translations. Let's think about the word set: it has thousands of meanings, and so it may have thousands of translations. Which is the correct one? It depends on the context where this word is used. It is possible to specify the string context by using the tc() function: it's very similar to t(), but its first parameter is a string describing the context. For instance, we may have:

echo tc('A group of items', 'Set');

and also

echo tc('Apply a value to an option', 'Set');

Using contexts allows having different translations for the same English text. Please remark that the strings A group of items and Apply a value to an option won't be included in the translation, they are used only to have different translations for the same word Set, and they will be visible to translators, helping them to better understand the English text.

You may have some doubts about using or not contexts (that is, t() vs tc()). Using too many tc() will unnecessary increase the translation work, using too few tc() will cause problems to translators and it may lead to senseless translated web pages.

As a general rule, translatable texts that are quite long (e.g. composed by three or more words) shouldn't require contexts, since their context may be self-defined and their meaning can't be misunderstood. For short texts... it depends. If a word may have various meanings (like set) you should use contexts (so tc() instead of t()); if a word has just one meaning (like Ok) you shouldn't use contexts (so t() instead of tc()).

Either way, listen carefully to translators' feedback: it's the only way to do things in the right way.

Core contexts

In order to avoid translations problems, Concrete implements with the following core contexts:

  • AreaName area names
  • AttributeKeyName names of the attribute keys
  • AttributeSetName names of the attribute sets
  • AttributeTypeName names of the attribute types
  • BlockTypeSetName names of the block sets
  • ConversationEditorName names of the editors
  • ConversationRatingTypeName names of the rating types of the conversations
  • FeedDescription descriptions of the feeds
  • FeedTitle titles of the feeds
  • GatheringDataSourceName names of the gathering data sources
  • GatheringItemTemplateName names of the gathering item templates
  • GroupDescription descriptions of the user groups
  • GroupName names of the user groups
  • ImageEditorControlSetName names of the control sets of the image editor
  • ImageEditorFilterName names of the filters of the image editor
  • JobSetName names of the job sets
  • PageTemplateName names of the page templates
  • PageTypeComposerControlName names of the controls of the page type composers
  • PageTypeComposerControlTypeName names of the types controls of the page type composers
  • PageTypeComposerFormLayoutSetControlCustomLabel names of the set of layouts of the page type composers
  • PageTypeComposerFormLayoutSetName names of the sets of page type composer forms
  • PageTypePublishTargetTypeName names of the targets of page types
  • PermissionAccessEntityTypeName names of permission access entity types
  • PermissionKeyDescription descriptions of permission keys
  • PermissionKeyName names of permission keys
  • PresetName names of style presets
  • SelectAttributeValue values of the select attribute types
  • StyleName names of the styles
  • StyleSetName names of the set of styles
  • SystemContentEditorSnippetName names of the snippets of the content editor
  • TemplateFileName names associated to template files
  • ThumbnailTypeName names of the types of the image thumbnails
  • Topic topics
  • TopicCategoryName names of the topic categories
  • TopicName topic names
  • TreeName names of the trees

Dates and times

In php, if you want the English name of the current month, you write this:

echo date('F');

But how can you retrieve the name of the month in the current site locale?

In Concrete there's the Date helper, that, as its name says, helps you with dates. It's very simple to use:

$dateHelper = \Core::make('helper/date');

In order to get the localized month name, you simply use its date method instead of the PHP built-in date function:

echo $dateHelper->date('F');

That's it. Please note that this date method accepts the same format string used by the standard php date() function.

Furthermore, you may want to format a date. To write the current date in a long format, in English you can call

echo date('F d, Y');

You could think to write something like echo $dateHelper->date('F d, Y');, but this assumes that in every language a date is written as the month name (F) followed by the day in the month (d), a comma and the year (Y). This is wrong.

For a correct date and/or time localization you should use the following ready-to-use methods of the Date helper:

  • $dateHelper->formatDate() to format a date only (reference)
  • $dateHelper->formatTime() to format a time only (reference)
  • $dateHelper->formatDateTime() to format a date and time (reference)

The date helper offers also a lot of other useful date-related functions: take a look at the API docs.

Translating packages

So, you've developed your own package. Great! You did everything possible to make it a thing that will change the world. And to let the whole world use it, you enabled localization by writing all those t() functions.

And now? How could your ready-to-be-translated package speak other languages?

You need to extract the translatable strings from your PHP files and create one language file for every language you plan to translate.

Starting from version 5.7.5.4, you can use Concrete itself to do it: open a console window and type the following command (replace the / with \ if you use Windows):

PATH-TO-CONCRETE5-ROOT-FOLDER/concrete/bin/concrete5 c5:package-translate PACKAGE_HANDLE OPTIONS

where:

  • PATH-TO-CONCRETE5-ROOT-FOLDER is the path of your Concrete installation directory
  • PACKAGE_HANDLE is the handle of your package (or the path to the directory that contains it)
  • OPTIONS use --help for a list of all the available options. The most important one is --locale (or its short form -l) that allows you to specify which language files you want to create.

For instance, let's assume you developed a package identified by the handyman handle. To extract the translations and create the language files for German and Italian simply write:

concrete5 c5:package-translate handyman -l it_IT -l de_DE

This command actually creates these files:

  • packages/handyman/languages/messages.pot this is the so-called gettext Portable Object Template file: it contains all the translatable strings found in your package; it could be used to create translation files for new languages, but since c5:package-translate is smart enough to create the language files for you, you can ignore it.
  • packages/handyman/languages/it_IT/LC_MESSAGES/messages.po this is the so-called Portable Object file that should be translated into Italian.
  • packages/handyman/languages/it_IT/LC_MESSAGES/messages.mo this is the so-called Machine Object file and it's the compiled version of the .po file.
  • packages/handyman/languages/de_DE/LC_MESSAGES/messages.po same as the file under it_IT but for German
  • packages/handyman/languages/de_DE/LC_MESSAGES/messages.mo same as the file under it_IT but for German

So, what should be done now? Translators have to translate the .po files. They can use with a normal text editor that supports UTF-8, but it's much easier to use one of the the many programs available for this purpose (for instance, POEdit, BetterPOEditor, ...)

Concrete can't use these .po files directly, they need to be compiled to the .mo format. In order to perform this compilation, you simply call the c5:package-translate command-line command again (only with the package handle):

concrete5 c5:package-translate handyman

This command will see that you have the .po files for Italian and German, and it will compile them to the .mo format (you can also use another command-line command: c5:package-pack).

After compiling the .po files, in order to see the new translations in your websites you have to clear the Concrete cache (via the Concrete dashboard or with the c5:clear-cache command line command).

PS: when you distribute your package, you can omit to include the .pot and .po files, since Concrete will only need the .mo files. The c5:package-pack can do this for you.