Contributing to internationalization (i18n)

Thanks to our contributors, FreshRSS is translated into more than 20 languages. This section will explain the basics of internationalization in FreshRSS, from translating the application to your own language to making a specific change.


It is common (and that’s an understatement) to want to show some text to the user. The problem is that FreshRSS has users of different nationalities. It is therefore necessary to be able to manage different languages in order not to remain confined to English or French.

The solution is to use the Minz_Translate module, which allows dynamic translation of FreshRSS. Before using this module, it is necessary to know where to find the strings to be translated. Each language has its own subdirectory in a parent directory named app/i18n/. For example, English language files are located in app/i18n/en/. There are seven different files:

This organization makes it possible to avoid a single huge translation file.

The translation files are quite simple: it’s only a matter of returning a PHP array containing the translations. As an example, here’s an extract from app/i18n/fr/gen.php:

return array(
	'action' => array(
		'actualize' => 'Actualiser',
		'back_to_rss_feeds' => '← Retour à vos flux RSS',
		'cancel' => 'Annuler',
		'create' => 'Créer',
		'disable' => 'Désactiver',
	'freshrss' => array(
		'_' => 'FreshRSS',
		'about' => 'À propos de FreshRSS',
	// ...

Each value can be referenced by a key: it consists of a series of identifiers separated by dots. The first identifier indicates from which file to extract the translation, while the following ones indicate array entries. Thus, the gen.freshrss.about key is referencing the about entry from the freshrss entry which is part of the main array returned by the gen.php file. This allows us to further organize our translation files.

You should not have to write the array by yourself and we provide several commands to ease the manipulation of these files. Let’s see some common use cases.

Add support for a new language

If you want to add support for a language which isn’t supported by FreshRSS yet, you can run this command:

make i18n-add-language lang=[your language code]

You must replace [your language code] by the language tag of your language. It must follow the IETF BCP 47 standard. For instance, English is en and French is fr. You can target a specific region with a subtag, for instance pt-br for Brazilian Portuguese. If you’re not sure of the code, Wikipedia might be a good start to find it or you can ask us for help too.

The command will create a new subfolder under app/i18n/ and copy the strings from the reference language (i.e. English). It will also mark all the translations with a special tag represented by a comment: // TODO - Translation. We’ll see in the next section how to translate the strings.

Translate the interface

You might have noticed some strings are not yet translated from English even though you’ve selected a different language. This is because we mostly speak English or French and it’s pretty difficult to us to speak all the different languages!

To update a string, you just have to open its file, find the string, and change it (without removing the quotes around it!) You might want to remove the comment at the end of the line, but you should prefer to use the following command:

make i18n-format

It will remove the comments on the lines that you’ve changed, and will reformat the file correctly. If you’ve made any mistakes, it will fix them automatically or it will tell you it can’t (well… the command will dramatically fail without any damage, don’t worry).

The strings to translate can be easily found in the translations files thanks to the tag we spoke about at the end of the previous section. Indeed, it indicates to our tools that the strings are not translated yet. This means you can find them with Git. For instance for the Greek language:

git grep TODO app/i18n/he

Acknowledge a false-positive

Our tool detects if a string needs to be translated if it equals to the English version. For instance, the word “version” is the same in English and French. Thus, our tool would mark the French word to be translated. This is, in fact, the case for the index.about.version key. This case is considered as a false-positive because the word is actually translated. To acknowledge such translations, you can run:

make i18n-ignore-key lang=fr key=index.about.version

This command adds an IGNORE comment on the translation so the key can be considered as translated.

Add/remove/update a key

If you’re developing a new part of the application, you might want to declare a new translation key. Your first impulse would be to add the key to each file manually: don’t do that, it’s very painful. We provide another command:

make i18n-add-key value='Your string in English'

This adds the key to all the files. It’ll be in English, waiting for other translators.

Conversely, you may want to remove a key that is no longer used in the application with:

make i18n-remove-key

Finally, if the English version of a string needs to be changed, you need to consider two cases. If the change doesn’t impact the meaning of the sentence, and therefore other languages don’t need to change (e.g. to fix a typo), you should make the change manually in the file. In any other case, you should use the following command:

make i18n-update-key value='The new string in English'

The key will simply be removed and added back with the new value.

How to access a translation programmatically

To access these translations, you must use the _t() function (which is a shortcut for Minz_Translate::t()). Code example:

	<?= _t('gen.freshrss.about') ?>

The function expects a translation key, but there’s a special case that sometimes makes life easier: the _ identifier. This must necessarily be present at the end of the chain and gives a value to the higher-level identifier. It’s pretty hard to explain but very simple to understand. In the example given above, an _ is associated with the value FreshRSS: this means that there is no need to write _t('gen.freshrss._') but _t('gen.freshrss') suffices.

_t() can take any number of variables. The variables will then be replaced in the translation if it contains some “conversion specifications” (usually %s or %d). You can learn more about these specifications in the sprintf() PHP function documentation.

For instance, the English translation for gen.auth.keep_logged_in is Keep me logged in <small>(%s days)</small>. It means this translation expects a string to be passed as an argument to the t() function (well, it should be a %d because we want a number here, but it doesn’t matter). For instance:

	<input type="checkbox" name="keep_logged_in" />
	<?= _t('gen.auth.keep_logged_in', 30) ?>