Version 3 end of life
This version of Silverstripe CMS will not recieve any additional bug fixes or documentation updates. Go to documentation for the most recent stable version.

i18n

The i18n class (short for "internationalization") in SilverStripe enables you to display templates and PHP code in different languages based on your global settings and the preferences of your website users. This process is also known as l10n (short for "localization").

For translating any content managed through the CMS or stored in the database, please use the translatable module.

This page aims to describe the low-level functionality of the i18n API. It targets developers who:

  • Are involved in creating templates in different languages.
  • Want to build their own modules with i18n capabilities.
  • Want to make their PHP-code (e.g. form labels) i18n-ready

Usage

Enabling i18n

The i18n class is enabled by default.

Setting the locale

To set the locale you just need to call i18n::set_locale() passing, as a parameter, the name of the locale that you want to set.

	// mysite/_config.php
	i18n::set_locale('de_DE'); // Setting the locale to German (Germany)
	i18n::set_locale('ca_AD'); // Setting to Catalan (Andorra)

Once we set a locale, all the calls to the translator function will return strings according to the set locale value, if these translations are available. See unicode.org for a complete listing of available locales.

Getting the locale

As you set the locale you can also get the current value, just by calling i18n::get_locale().

Declaring the content language in HTML

To let browsers know which language they're displaying a document in, you can declare a language in your template.

	//'Page.ss' (HTML)
	<html lang="$ContentLocale">

	//'Page.ss' (XHTML)
	<html lang="$ContentLocale" xml:lang="$ContentLocale" xmlns="http://www.w3.org/1999/xhtml">

Setting the <html> attribute is the most commonly used technique. There are other ways to specify content languages (meta tags, HTTP headers), explained in this w3.org article.

You can also set the script direction, which is determined by the current locale, in order to indicate the preferred flow of characters and default alignment of paragraphs and tables to browsers.

	<html lang="$ContentLocale" dir="$i18nScriptDirection">

Formats can be set globally in the i18n class. These settings are currently only picked up by the CMS, you'll need to write your own logic for any frontend output.

	Config::inst()->update('i18n', 'date_format', 'dd.MM.YYYY');
	Config::inst()->update('i18n', 'time_format', 'HH:mm');

This means all formats are defined in ISO date format, not PHP's built-in date().

Language Names

SilverStripe comes with a built-in list of common languages, listed by locale and region. They can be accessed via the i18n.common_languages and i18n.common_locales config setting.

In order to add a value, add the following to your config.yml:

	i18n:
	  common_locales:
	    de_CGN:
	      name: German (Cologne)
	      native: Kölsch
	i18n:
	  common_locales:
	    en_NZ:
	      native: Niu Zillund

By default, URLs for pages in SilverStripe (the SiteTree->URLSegment property) are automatically reduced to the allowed allowed subset of ASCII characters. If characters outside this subset are added, they are either removed or (if possible) "transliterated". This describes the process of converting from one character set to another while keeping characters recognizeable. For example, vowels with french accents are replaced with their base characters, pâté becomes pate.

In order to allow for so called "multibyte" characters outside of the ASCII subset, limit the character filtering in the underlying configuration setting, by setting URLSegmentFilter.default_use_transliterator to false in your YAML configuration.

Please refer to W3C: Introduction to IDN and IRI for more details.

i18n in Form Fields

Date- and time related form fields support i18n (DateField, TimeField, DatetimeField).

	i18n::set_locale('ca_AD');
	$field = new DateField(); // will automatically set date format defaults for 'ca_AD'
	$field->setLocale('de_DE'); // will not update the date formats
	$field->setConfig('dateformat', 'dd. MMMM YYYY'); // sets typical 'de_DE' date format, shows as "23. Juni 1982"

and TimeField.default_config configuration arrays. If no 'locale' default is set on the field, i18n::get_locale() will be used.

Important: Form fields in the CMS are automatically configured according to the profile settings for the logged-in user (Member->Locale, Member->DateFormat and Member->TimeFormat). This means that in most cases, fields created through DataObject::getCMSFields() will get their i18n settings from a specific member

The DateField API can be enhanced by JavaScript, and comes with jQuery UI datepicker capabilities built-in. The field tries to translate the date formats and locales into a format compatible with jQuery UI (see DateFieldView_JQuery::$locale_map and DateField_View_JQuery::convert_iso_to_jquery_format()).

	$field = new DateField();
	$field->setLocale('de_AT'); // set Austrian/German locale
	$field->setConfig('showcalendar', true);
	$field->setConfig('jslocale', 'de'); // jQuery UI only has a generic German localization
	$field->setConfig('dateformat', 'dd. MMMM YYYY'); // will be transformed to 'dd. MM yy' for jQuery

Adapting a module to make it localizable is easy with SilverStripe. You just need to avoid hardcoding strings that are language-dependent and use a translator function call instead.

	// without i18n
	echo "This is a string";
	// with i18n
	echo _t("Namespace.Entity","This is a string");

All strings passed through the _t() function will be collected in a separate language table (see Collecting text), which is the starting point for translations.

The _t() function

The _t() function is the main gateway to localized text, and takes four parameters, all but the first being optional. It can be used to translate strings in both PHP files and template files. The usage for each case is described below.

  • $entity: Unique identifier, composed by a namespace and an entity name, with a dot separating them. Both are arbitrary names, although by convention we use the name of the containing class or template. Use this identifier to reference the same translation elsewhere in your code.
  • $string: (optional) The original language string to be translated. Only needs to be declared once, and gets picked up the text collector.
  • $string: (optional) Natural language comment (particularly short phrases and individual words) are very context dependent. This parameter allows the developer to convey this information to the translator.
  • $array:: (optional) An array of injecting variables into the second parameter

Usage in PHP Files

	// Simple string translation
	_t('LeftAndMain.FILESIMAGES','Files & Images');

	// Using the natural language comment parameter to supply additional context information to translators
	_t('LeftAndMain.HELLO','Site content','Menu title');

	// Using injection to add variables into the translated strings.
	_t('CMSMain.RESTORED',
		"Restored {value} successfully",
		'This is a message when restoring a broken part of the CMS',
		array('value' => $itemRestored)
	);

[hint] The preferred template syntax has changed somewhat since version 2.x. [/hint]

In .ss template files, instead of _t(params) the syntax <%t params %> is used. The syntax for passing parameters to the function is quite different to the PHP version of the function.

  • Parameters are space separated, not comma separated
  • The original language string and the natural language comment parameters are separated by on.
  • The final parameter (which is an array in PHP) is passed as a space separated list of key/value pairs.
	// Simple string translation
	<%t Namespace.Entity "String to translate" %>

	// Using the natural language comment parameter to supply additional context information to translators
	<%t SearchResults.NoResult "There are no results matching your query." is "A message displayed to users when the search produces no results." %>

	// Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope).
	<%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %>

When caching a <% loop %> or <% with %> with <%t params %>. It is important to add the Locale to the cache key otherwise it won't pick up locale changes.

	<% cached 'MyIdentifier', $CurrentLocale %>
		<% loop $Students %>
			$Name
		<% end_loop %>
	<% end_cached %>

To collect all the text in code and template files we have just to visit: http://localhost/dev/tasks/i18nTextCollectorTask

Text collector will then read the files, build the master string table for each module where it finds calls to the underscore function, and tell you about the created files and any possible entity redeclaration.

If you want to run the text collector for just one module you can use the 'module' parameter: http://localhost/dev/tasks/i18nTextCollectorTask/?module=cms

[hint] You'll need to install PHPUnit to run the text collector (see testing-guide). [/hint]

Module Priority

The order in which i18n strings are loaded from modules can be quite important, as it is pretty common for a site developer to want to override the default i18n strings from time to time. Because of this, you will sometimes need to specify the loading priority of i18n modules.

By default, the language files are loaded from modules in this order:

  • Your project (as defined in the $project global)
  • admin
  • framework
  • All other modules

This default order is configured in framework/_config/i18n.yml. This file specifies two blocks of module ordering: basei18n, listing admin, and framework, and defaulti18n listing all other modules.

To create a custom module order, you need to specify a config fragment that inserts itself either after or before those items. For example, you may have a number of modules that have to come after the framework/admin, but before anyhting else. To do that, you would use this

---
	Name: customi18n
	Before: 'defaulti18n'
	i18n:
	  module_priority:
	    - module1
	    - module2
	    - module3

There are a few special cases:

  • If not explicitly mentioned, your project is put as the first module.
  • The module name other_modules can be used as a placeholder for all modules that aren't specifically mentioned.

Language definitions

Each module can have one language table per locale, stored by convention in the lang/ subfolder. The translation is powered by Zend_Translate, which supports different translation adapters, dealing with different storage formats.

By default, SilverStripe 3.x uses a YAML format (through the Zend_Translate_RailsYAML adapter).

Example: framework/lang/en.yml (extract)

	en:
	  ImageUploader:
	    Attach: 'Attach %s'
	  UploadField:
	    NOTEADDFILES: 'You can add files once you have saved for the first time.'
	de:
	  ImageUploader:
	    ATTACH: '%s anhängen'
	  UploadField:
	    NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben'

The cache can be cleared through the ?flush=1 query parameter, or explicitly through Zend_Translate::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL).

[hint] The format of language definitions has changed significantly in since version 2.x. [/hint]

In order to enable usage of version 2.x style language definitions in 3.x, you need to register a legacy adapter in your mysite/_config.php:

	i18n::register_translator(
		new Zend_Translate(array(
			'adapter' => 'i18nSSLegacyAdapter',
			'locale' => i18n::default_locale(),
			'disableNotices' => true,
		)),
		'legacy',
		9 // priority lower than standard translator
	);

The i18n system in JavaScript is similar to its PHP equivalent. Languages are typically stored in <my-module-dir>/javascript/lang. Unlike the PHP logic, these files aren't auto-discovered and have to be included manually.

Requirements

Each language has its own language table in a separate file. To save bandwidth, only two files are actually loaded by the browser: The current locale, and the default locale as a fallback. The Requirements class has a special method to determine these includes: Just point it to a directory instead of a file, and the class will figure out the includes.

	Requirements::add_i18n_javascript('<my-module-dir>/javascript/lang');

Translation Tables in JavaScript

Translation tables are automatically included as required, depending on the configured locale in i18n::get_locale(). As a fallback for partially translated tables we always include the master table (en.js) as well.

Master Table (<my-module-dir>/javascript/lang/en.js)

	if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') {
	  console.error('Class ss.i18n not defined');
	} else {
	  ss.i18n.addDictionary('en', {
	    'MYMODULE.MYENTITY' : "Really delete these articles?"
	  });
	}

Example Translation Table (<my-module-dir>/javascript/lang/de.js)

	ss.i18n.addDictionary('de', {
	  'MYMODULE.MYENTITY' : "Artikel wirklich löschen?"
	});

build task, with the actual source files in a JSON format which can be processed more easily by external translation providers (see javascript/lang/src).

Basic Usage

	alert(ss.i18n._t('MYMODULE.MYENTITY'));

Advanced Use

The ss.i18n object contain a couple functions to help and replace dynamic variable from within a string.

Legacy sequential replacement with sprintf()

	// MYMODULE.MYENTITY contains "Really delete %s articles by %s?"
	alert(ss.i18n.sprintf(
		ss.i18n._t('MYMODULE.MYENTITY'),
		42,
		'Douglas Adams'
	));
	// Displays: "Really delete 42 articles by Douglas Adams?"

Variable injection with inject()

	// MYMODULE.MYENTITY contains "Really delete {count} articles by {author}?"
	alert(ss.i18n.inject(
		ss.i18n._t('MYMODULE.MYENTITY'),
		{count: 42, author: 'Douglas Adams'}
	));
	// Displays: "Really delete 42 articles by Douglas Adams?"

Limitations

  • No detecting/conversion of character encodings (we rely fully on UTF-8)
  • Translation of graphics/assets
  • Usage of gettext (too clumsy, too many requirements)
  • Displaying multiple languages/encodings on the same page

Links