Joomla Language Files and Internationalisation
|
This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production. This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages. |
Every string that an extension shows to a person is a candidate for translation. Joomla solves this with language
constants: the PHP code and the layouts never contain the visible words, only keys such as
COM_TALLPINE_TRAIL_DISTANCE_LABEL, and each installed language supplies a plain text file that maps the keys to the
words. This page is the developer’s view of that mechanism: the format and location of the .ini files, how they are
registered in a manifest, the functions of the Text class (Text::_(), Text::alt(), Text::sprintf(),
Text::plural(), Text::script()), strings for JavaScript, language overrides, the cache that parses the files in
Joomla 6.0 and later, and how language files and translation packs are packaged.
The page was checked against the Joomla Developer Manual and the 6.2 source tree on 2026-10-30. It refers to Joomla 6.2
(the current release, requiring PHP 8.3 or newer). The language system has been stable since Joomla 4 and the text
marks the differences between 3.x, 4.x, 5.x and 6.x. Joomla 5.4 only receives security fixes, and 4.x and 3.x are end of
life (see Versions, Release Cycle and Support). The site
builder’s side (installing languages, content languages, the language switcher, associations) is described in
Joomla Multilingual Sites. The plain-PHP functions that the examples rely on
(sprintf, string handling and Unicode) are covered in PHP strings.
The examples belong to the Tallpine Hiking Club, a fictitious mountain club described in
Joomla Introduction and Architecture. Its package pkg_tallpine
contains the component com_tallpine, the module mod_tallpine_nexthikes and the plugin plg_content_tallpine_conditions.
The site is published in English (en-GB, the default) and German (de-DE).
The idea: constants instead of words
A language constant is an upper-case key. The lookup is case-insensitive in the sense that the language class converts
the requested key with strtoupper() before it consults its table, but by convention keys are written in upper case in
both the files and the code. A key is made of:
-
a prefix that names the extension:
COM_TALLPINE_,MOD_TALLPINE_NEXTHIKES_,PLG_CONTENT_TALLPINE_CONDITIONS_,TPL_CASSIOPEIA_TALLPINE_, or the core prefixesJGLOBAL_,JERROR_,JLIB_and the shortJ...constants such asJYES,JNO,JALL,JSAVE -
a section that says where the text is used:
FIELD_,TRAIL_,MSG_,ERROR_ -
a suffix that says what the text is:
_LABELfor a form label,_DESCfor its tooltip,_TITLE,_MSG,_HEADING
Using the extension prefix keeps keys unique, because all language files loaded during a request are merged into one
table. Using the same key in two files does not raise an error, the file loaded later simply wins. Never reuse another
extension’s prefix, and never use a key that is a reserved word of the INI format (YES, NO, NULL, FALSE, ON,
OFF, NONE, TRUE), because the debug checker of the language class reports them as problems.
Language file format
A language file is a PHP-style INI file, parsed with parse_ini_file() in its raw mode, so that no value is interpreted
(yes, 1 or on stay strings) and no PHP constants are expanded. The rules are small:
-
one
KEY="Translation"pair per line, with the value always in double quotes -
a line that starts with a semicolon is a comment, and blank lines are ignored
-
keys contain
A-Z,0-9and underscores, and start with a letter (the debug checker matches the pattern^[A-Z][A-Z0-9_:\*\-\.]*\s*=\s*".*") -
a double quote inside a value is written
\" -
a percent sign that must appear literally is written
%%, because the strings are used assprintfformats -
the file is UTF-8 without a byte order mark
-
\nand\tinside a value are converted to a line feed and a tab byText::_()(when its third argument is true, which is the default), and\\becomes a single backslash
; language/en-GB/com_tallpine.ini
; Tallpine Hiking Club, site and administrator strings of com_tallpine
COM_TALLPINE="Tallpine trails"
COM_TALLPINE_TRAIL_DISTANCE_LABEL="Distance (km)"
COM_TALLPINE_TRAIL_DISTANCE_DESC="Length of the trail in kilometres, one way."
COM_TALLPINE_TRAIL_DIFFICULTY_LABEL="Difficulty"
COM_TALLPINE_TRAIL_MSG_SAVED="Trail \"%s\" was saved."
COM_TALLPINE_TRAIL_MSG_COMPLETION="You completed %d%% of the route."
COM_TALLPINE_ERROR_TRAIL_NOT_FOUND="The trail could not be found."
COM_TALLPINE_TRAIL_N_HIKES_COUNT="%d hikes planned."
COM_TALLPINE_TRAIL_N_HIKES_COUNT_0="No hikes planned."
COM_TALLPINE_TRAIL_N_HIKES_COUNT_1="One hike planned."
; language/de-DE/com_tallpine.ini
COM_TALLPINE="Tallpine Touren"
COM_TALLPINE_TRAIL_DISTANCE_LABEL="Distanz (km)"
COM_TALLPINE_TRAIL_DISTANCE_DESC="Länge der Tour in Kilometern, einfache Strecke."
COM_TALLPINE_TRAIL_DIFFICULTY_LABEL="Schwierigkeit"
COM_TALLPINE_TRAIL_MSG_SAVED="Die Tour \"%s\" wurde gespeichert."
COM_TALLPINE_TRAIL_MSG_COMPLETION="Du hast %d%% der Route geschafft."
COM_TALLPINE_ERROR_TRAIL_NOT_FOUND="Die Tour wurde nicht gefunden."
COM_TALLPINE_TRAIL_N_HIKES_COUNT="%d Wanderungen geplant."
COM_TALLPINE_TRAIL_N_HIKES_COUNT_0="Keine Wanderungen geplant."
COM_TALLPINE_TRAIL_N_HIKES_COUNT_1="Eine Wanderung geplant."
Source: Joomla Manual: Multilingual
Older tutorials and the Joomla 3 wiki write a double quote inside a value as the token "_QQ_". The Joomla 6.2 language
class and its INI reader contain no handling of that token. The reader only restores the escaped quote \", so new
files must use \". A file that comes from a Joomla 3 extension and still contains "_QQ_" shows the token literally,
and must be edited.
The two kinds of file: .ini and .sys.ini
Each extension normally ships two files per language:
| File | Content |
|---|---|
|
The strings that the running extension uses: form labels, messages, layout text, the names of views. It is loaded when the extension is used. |
|
The system strings: the extension’s name and description in the installer and in the Extensions: Manage list, the menu item types of a component (title and description of each view shown in the menu item dialog), the title of a module or plugin in the lists, and the access-control section names. Joomla loads it in the administrator in places where the extension itself is not running. |
; language/en-GB/com_tallpine.sys.ini
COM_TALLPINE="Tallpine trails"
COM_TALLPINE_XML_DESCRIPTION="Trail reports and planned hikes of the Tallpine Hiking Club."
COM_TALLPINE_TRAILS_VIEW_DEFAULT_TITLE="List of trails"
COM_TALLPINE_TRAILS_VIEW_DEFAULT_DESC="Shows all published trails of a category."
COM_TALLPINE_TRAIL_VIEW_DEFAULT_TITLE="Single trail"
Source: Joomla Manual: Multilingual
A frequent mistake is to put a key in com_tallpine.ini that the installer or the menu item dialog needs. Those screens
only load the .sys.ini file, so the raw constant is displayed there. Another is to put every string in the .sys.ini
file: it is loaded in the administrator on pages that list extensions, so it should stay small.
File names and locations
Joomla 4 changed the file naming. Joomla 3 prefixed each file with the language tag (en-GB.com_tallpine.ini). Since
Joomla 4 the name is just <extension>.ini, and the language is given by the folder:
com_tallpine/
admin/
language/
en-GB/
com_tallpine.ini
com_tallpine.sys.ini
de-DE/
com_tallpine.ini
com_tallpine.sys.ini
site/
language/
en-GB/com_tallpine.ini
de-DE/com_tallpine.ini
mod_tallpine_nexthikes/
language/
en-GB/
mod_tallpine_nexthikes.ini
mod_tallpine_nexthikes.sys.ini
plg_content_tallpine_conditions/
language/
en-GB/
plg_content_tallpine_conditions.ini
plg_content_tallpine_conditions.sys.ini
The loader still accepts the old name with the language prefix (it tries <extension>.ini first and then
<lang>.<extension>.ini), which is why Joomla 3 extensions keep working, but new code uses the short name. At
installation the files are copied to one of two roots:
| Root | Used by | Contents |
|---|---|---|
|
The site (front end) |
Strings of site components, modules, plugins and templates that run in the front end |
|
The administrator |
Strings of administrator components, administrator modules, plugins and templates that run in the back end |
Third root: the Web Services API application (api/language/, the JPATH_API path) exists for the API client. Strings
of API responses come from the com_tallpine files that the API application loads; see
Web Services API.
Only the folder of the extension in the package decides the destination. An administrator-only module that is translated
into German must carry its files in the administrator client of the package, otherwise the strings are copied to the
wrong root and the module’s heading appears as a raw constant.
An extension can also keep its strings in its own folder and load them with an explicit base path, as shown in the loading section below.
Registering language files in the manifest
The installer copies the language files that the extension manifest lists in a <languages> element. Each <language>
has a tag attribute and the path of the file inside the package:
<?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade">
<name>com_tallpine</name>
<namespace path="src">Tallpine\Component\Tallpine</namespace>
<version>1.0.0</version>
<description>COM_TALLPINE_XML_DESCRIPTION</description>
<files folder="site">
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="site/language">
<language tag="en-GB">en-GB/com_tallpine.ini</language>
<language tag="de-DE">de-DE/com_tallpine.ini</language>
</languages>
<administration>
<menu>COM_TALLPINE</menu>
<files folder="admin">
<folder>services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="admin/language">
<language tag="en-GB">en-GB/com_tallpine.ini</language>
<language tag="en-GB">en-GB/com_tallpine.sys.ini</language>
<language tag="de-DE">de-DE/com_tallpine.ini</language>
<language tag="de-DE">de-DE/com_tallpine.sys.ini</language>
</languages>
</administration>
</extension>
Source: Joomla Manual: Multilingual
A module or plugin has a single <languages> element at the top level of its manifest, as the core plugin
plg_content_joomla does:
<languages>
<language tag="en-GB">language/en-GB/mod_tallpine_nexthikes.ini</language>
<language tag="en-GB">language/en-GB/mod_tallpine_nexthikes.sys.ini</language>
<language tag="de-DE">language/de-DE/mod_tallpine_nexthikes.ini</language>
<language tag="de-DE">language/de-DE/mod_tallpine_nexthikes.sys.ini</language>
</languages>
Source: Joomla Manual: Manifest files
The <name> and <description> elements and the menu entries of a manifest hold constants too, as in
<description>COM_TALLPINE_XML_DESCRIPTION</description>. The installer translates them with the .sys.ini file of the
extension. See Extension Types, Manifests and Packaging for
the rest of the manifest.
A file that is missing from <languages> is not copied, so a translation added to the source tree but not to the manifest
works on the developer’s machine and disappears in the installed package. The same applies to a plugin: it is a frequent
source of "works locally, raw constants in production" reports.
Loading the language files
Registering the files makes them available; something must also load them. Joomla loads them in these places:
-
Core language files (
joomla.iniand the files of the core extensions that the application always needs) are loaded by the application at the start of the request. -
Components are loaded by the component dispatcher. In the MVC code of Joomla 4 and later the
ComponentDispatcherloads the component’s.inifile for the current client before it dispatches the controller, soText::_()works in every controller, model and layout of the component. -
Modules are loaded by the module renderer before the dispatcher runs, using the module’s own folder as a source.
-
Templates load
tpl_<name>.iniwhen the template is initialised. -
Plugins must ask for their files. A plugin sets the property
$autoloadLanguagetotruein its class to have itsplg_<group>_<name>.iniloaded when the plugin object is created. In Joomla 6.0 that load became afinalmethod ofCMSPlugin(autoloadLanguage()) that guards against loading twice and falls back to the global application if the plugin has none. A plugin that has no language strings of its own must not set the flag, because it costs a file load on every request.
<?php
namespace Tallpine\Plugin\Content\TallpineConditions\Extension;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use Joomla\CMS\Event\Content\ContentPrepareEvent;
\defined('_JEXEC') or die;
final class TallpineConditions extends CMSPlugin implements SubscriberInterface
{
/**
* Load plg_content_tallpine_conditions.ini for the current client.
*/
protected $autoloadLanguage = true;
public static function getSubscribedEvents(): array
{
return ['onContentPrepare' => 'onContentPrepare'];
}
public function onContentPrepare(ContentPrepareEvent $event): void
{
$item = $event->getItem();
if (!isset($item->text) || !str_contains($item->text, '{conditions}')) {
return;
}
$item->text = str_replace(
'{conditions}',
'<p class="trail-conditions">' . Text::_('PLG_CONTENT_TALLPINE_CONDITIONS_NOTICE') . '</p>',
$item->text
);
}
}
Source: Joomla Manual: How plugins work
For explicit loading, use the language object of the application and give it the extension and a base path. A code path that does not run inside the component dispatcher (a console command, a scheduled task, a helper called from another extension) needs this:
<?php
use Joomla\CMS\Factory;
\defined('_JEXEC') or die;
// Inside a class that has the application (the Aware traits give $this->getApplication()).
$language = $this->getApplication()->getLanguage();
// Load administrator/language/<tag>/com_tallpine.ini, and the site or default language first as fallback.
$language->load('com_tallpine', JPATH_ADMINISTRATOR);
// Load strings that sit in the component's own folder instead of the global language folder.
$language->load('com_tallpine', JPATH_ADMINISTRATOR . '/components/com_tallpine');
Source: Joomla Manual: Multilingual
Older books such as Developing Extensions for Joomla! 5 (2023) obtain the language object with
Factory::getLanguage(). In Joomla 6 prefer the language of the application ($app->getLanguage()) or the object
injected by the framework, in line with the dependency-injection style described in
Namespaces, Autoloading and Dependency Injection.
The static Text methods used below still work and call the language object internally.
The order in which strings are merged
The Language::load() method fills one table of constants per request. For a language that is not the default one, and
when debugging is off, it loads in this order, each step overwriting keys of the previous steps:
-
the default language of the site (
en-GBfor Tallpine), so that no constant stays raw when the German file lacks a key -
in Joomla 6.2, the fallback chain of the requested language, weakest first (for example
de-DEas the fallback ofde-CH), see Joomla Multilingual Sites -
the file of the requested language,
<extension>.inior, if it does not exist,<lang>.<extension>.ini -
the override file for the client and language, which is merged last so that it wins over everything
When Debug Language is on, the merge of default and fallback languages is skipped on purpose: a missing German string must not be hidden by the English one.
Text::_(): translating a key
Joomla\CMS\Language\Text is the static facade of the language system. Its central method translates a key:
<?php
use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
echo Text::_('COM_TALLPINE_TRAIL_DISTANCE_LABEL'); // Distance (km) / Distanz (km)
echo Text::_('JYES'); // core constant, translated by the core language file
The signature is Text::_($string, $jsSafe = false, $interpretBackSlashes = true, $script = false). What the arguments
do:
| Argument | Effect |
|---|---|
|
The key. It is converted to upper case before the lookup. If the key is not in the merged table, the string is returned
unchanged, so a missing translation shows the raw constant, for instance |
|
When true, the result is passed through |
|
When true (the default) and |
|
When true, the translated text is stored in the JavaScript string store and the key is returned (see JavaScript strings). |
The method also has a quirk that was added in Joomla 3.4.4: if the key passed to Text::_() contains a comma, the text is
split at the commas, the first part is used as a sprintf format and the others as its arguments. That behaviour exists
for old code; new code must call Text::sprintf() explicitly and never build a key from user input, because the user could
then choose which translations are shown.
Text::alt(): a variant of a key
Text::alt($string, $alt) uses the key $string . '_' . $alt if it exists and the key $string if not. It is used for
alternative wordings that depend on a context, for instance a different button text on a small screen:
COM_TALLPINE_SAVE="Save trail report"
COM_TALLPINE_SAVE_MOBILE="Save"
<?php
echo Text::alt('COM_TALLPINE_SAVE', 'mobile'); // "Save"; falls back to "Save trail report" if no _MOBILE key exists
Text::sprintf(): values inside a translation
Hard-coding a sentence in code and concatenating a value to it makes translation impossible, because the word order
differs between languages. A translation file holds the whole sentence with placeholders and the code passes the values.
Text::sprintf() translates the key and calls the PHP function sprintf() with the remaining arguments:
<?php
use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
$this->getApplication()->enqueueMessage(
Text::sprintf('COM_TALLPINE_TRAIL_MSG_SAVED', $trail->title),
'success'
);
// en-GB: Trail "Hochalm Loop" was saved.
// de-DE: Die Tour "Hochalm Loop" wurde gespeichert.
echo Text::sprintf('COM_TALLPINE_TRAIL_MSG_COMPLETION', 80); // You completed 80% of the route.
Source: Joomla Manual: Using variables
Text::printf() does the same but prints the result instead of returning it. It does not support the JavaScript or
placeholder options of sprintf().
Placeholders and positions
The placeholders are those of PHP’s sprintf(): %s for a string, %d for an integer, %f or %.1f for a floating-point
number, %05d for zero-padded numbers, and %% for a literal percent sign. When the translation needs a different word
order, a positional specifier numbers the arguments, so the translator can reorder them without a code change:
; en-GB
MOD_TALLPINE_NEXTHIKES_ROUTE="Hike %1$s starts at %2$s."
; de-DE: the order of the two values is reversed in German.
MOD_TALLPINE_NEXTHIKES_ROUTE="Ab %2$s startet die Wanderung %1$s."
<?php
echo Text::sprintf('MOD_TALLPINE_NEXTHIKES_ROUTE', $hike->title, $hike->meetingTime);
Source: Joomla Manual: Format specifiers
The Manual lists the specifiers b, c, d, e, u, f, F, o, s, x and X, and the order of a directive:
the percent sign, an optional sign, alignment (- for left-justified), padding (a zero or a character after a single
quote), width and precision, then the type. HTML collapses repeated spaces, so padding only shows in a preformatted
element.
Argument count
Too many arguments are ignored. Too few arguments are a fatal error: PHP 8 throws an ArgumentCountError
("2 arguments are required, 1 given"), and Joomla does not intercept it. This is the classic way a bad translation breaks
a site: a translator who adds a second %s to the German file ("%s von %s") while the code passes one value makes the
whole page fail in German only. The quality checks for a translation pack must therefore compare the number of
placeholders of every key between the source and the target language. Placeholders must never appear in a translation of a
key that the code uses with Text::_(), because Text::_() does not run sprintf(): a %s would be displayed literally.
An options array
The last argument of Text::sprintf() and of Text::plural() can be an array with the options jsSafe (default
false), interpretBackSlashes (default true) and script (default false), the same flags as the arguments of
Text::_().
<?php
$text = Text::sprintf('COM_TALLPINE_TRAIL_MSG_SAVED', $trail->title, ['jsSafe' => true]);
Source: Joomla Manual: Using variables
Text::plural(): counts
English distinguishes one from many, but Polish, Russian, Arabic and Welsh have more forms, and a sentence like "1 hikes"
is wrong in every language. Text::plural($key, $n) chooses a key according to the number and runs sprintf() with the
number as the first argument:
<?php
use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
echo Text::plural('COM_TALLPINE_TRAIL_N_HIKES_COUNT', count($hikes));
// 0 -> "No hikes planned." 1 -> "One hike planned." 5 -> "5 hikes planned."
Source: Joomla Manual: Plural
How the key is chosen
The base key is the fallback. Text::plural() builds a list of candidate suffixes and uses the first candidate key that
exists in the language table:
-
If
$nis not 1, the number itself is the first candidate. A key..._5or..._12therefore wins for exactly that number. The number is not added for 1, because several languages use the same form for 1 and for 21. -
Then the suffixes that the language provides. They come from the localise class of the language (the file
localise.phpin the language folder, the methodgetPluralSuffixes($count)), and without such a class the only suffix is the number. -
If no candidate key exists, the base key is used.
The English language pack returns ['0'] for zero, ['ONE', '1'] for one, and ['OTHER', 'MORE'] for anything else. For
the Tallpine file above this means: count 0 uses _0, count 1 uses _ONE if it exists and _1 otherwise, and for a
count of 5 the order is _5, _OTHER, _MORE, then the base key "%d hikes planned.". That is why the file defines the
generic form on the base key and only the special cases with suffixes.
; a number-specific form and the catch-all forms
MOD_TALLPINE_NEXTHIKES_N_HUTS="Huts"
MOD_TALLPINE_NEXTHIKES_N_HUTS_5="Exactly five huts are open."
MOD_TALLPINE_NEXTHIKES_N_HUTS_MORE="%d huts are open."
MOD_TALLPINE_NEXTHIKES_N_HUTS_OTHER="Many huts are open."
Source: Joomla Manual: Plural
When both _OTHER and _MORE exist, _OTHER is tried first. Removing _OTHER makes _MORE apply to the counts that
have no exact key.
Languages with more plural forms
A language pack that has more forms ships a localise file whose getPluralSuffixes() returns other names. The shape of the
core class is a static method:
<?php
// language/pl-PL/localise.php (shape of a localise class)
abstract class Pl_PLLocalise
{
public static function getPluralSuffixes($count)
{
if ($count == 1) {
return ['1'];
}
// two to four (but not twelve to fourteen) use the "few" form
$last = $count % 10;
$tens = $count % 100;
if ($last >= 2 && $last <= 4 && ($tens < 12 || $tens > 14)) {
return ['FEW'];
}
return ['MANY'];
}
}
The class name is the language tag with its hyphen removed and the case adapted (En_GBLocalise for en-GB), and the
method is looked up by the Language constructor. A translator who works on a language with several forms does not edit
PHP; the pack already contains the class, and the translator adds the keys with the suffixes that the class returns
(_FEW, _MANY in the example). The Tallpine extension should provide at least the base key plus _0 and _1, and give
translators a note in a comment of the .ini file on which suffixes the target language uses.
Writing translatable strings
A few rules avoid most translation problems:
-
Never build a sentence from pieces.
Text::_('A') . ' ' . $name . ' ' . Text::_('B')assumes a word order. Use one key with a placeholder. -
Do not translate fragments that are language-dependent, such as plurals added with "s". Use
Text::plural(). -
Put dynamic parts in placeholders, and keep markup out of the value wherever possible. If a message needs a link, pass the already-escaped link as an argument instead of translating the HTML.
-
Escape on output. A translation is trusted content, but the values that
Text::sprintf()inserts (a trail title, a user name) are not. Escape them withhtmlspecialchars()or the layout’s$this->escape()before they reach a message or a layout, see Security in Extensions. In a layout,$this->escape(Text::_(...))is the usual form for text placed inside an attribute. -
Choose precise keys. A "Date" label for a form and a "Date" column of a list may need different words in German; use two keys.
-
Provide a description (
_DESC) for every form field, since it becomes the tooltip. Forms refer to labels by key, see Forms and Form Fields.
<?php
use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
/** @var \Tallpine\Component\Tallpine\Site\View\Trail\HtmlView $this */
?>
<h2><?php echo $this->escape($this->item->title); ?></h2>
<dl>
<dt><?php echo Text::_('COM_TALLPINE_TRAIL_DISTANCE_LABEL'); ?></dt>
<dd><?php echo (float) $this->item->distance; ?></dd>
</dl>
<p><?php echo Text::plural('COM_TALLPINE_TRAIL_N_HIKES_COUNT', (int) $this->item->hikeCount); ?></p>
JavaScript language strings
JavaScript cannot read the PHP language table. Joomla transfers a chosen set of strings to the page as script options and
exposes them through Joomla.Text. It is a two-step process: the PHP code registers a key, and the script reads it:
<?php
use Joomla\CMS\Language\Text;
use Joomla\CMS\HTML\HTMLHelper;
\defined('_JEXEC') or die;
// In HtmlView::display(), a layout or a module dispatcher, before the page is rendered.
Text::script('COM_TALLPINE_CONFIRM_DELETE_TRAIL');
Text::script('COM_TALLPINE_ERROR_GPX_TOO_LARGE');
$wa = $this->getDocument()->getWebAssetManager();
$wa->getRegistry()->addRegistryFile('media/com_tallpine/joomla.asset.json');
$wa->useScript('com_tallpine.trail-form');
COM_TALLPINE_CONFIRM_DELETE_TRAIL="Delete this trail report permanently?"
COM_TALLPINE_ERROR_GPX_TOO_LARGE="The GPX file is larger than 5 MB."
// media/com_tallpine/js/trail-form.js
document.querySelector('#delete-trail').addEventListener('click', (event) => {
if (!window.confirm(Joomla.Text._('COM_TALLPINE_CONFIRM_DELETE_TRAIL'))) {
event.preventDefault();
}
});
What Text::script() does in the 6.2 source: it uppercases the key, translates it, stores the result in the static
string table Text::getScriptStrings() returns, and writes the table into the document’s script options under the name
joomla.jtext (it also makes sure the core web asset is loaded). Joomla.Text._('KEY') then reads that table in the
browser. Consequences:
-
A key must be registered before the document head is rendered. Calling
Text::script()after the page output has been sent has no effect. -
The script only receives strings that were registered, so a script that asks for an unregistered key gets an empty value. Registering is cheap, but each registered string increases the HTML of every page that uses it, so register keys where the script is used, not globally.
-
There is no
sprintfin JavaScript. Put a placeholder in the string ({count}) and replace it in the script, or build the string in PHP and register it with the options array (['script' => true]), which stores the formatted text. -
Text::script()called with no argument is deprecated since Joomla 3.7 and raises a deprecation notice; Joomla 7.0 does not support it. UseText::getScriptStrings()to read the registered strings. -
Data that is not a translation, such as a URL or a limit, goes through script options (
$document->addScriptOptions('com_tallpine.options', [...])andJoomla.getOptions()), not through language strings.
The asset registration in the example is the subject of Web Asset Manager.
Language overrides
An override replaces a string without touching the extension, so that an update does not undo the change. Overrides are
stored in language/overrides/<tag>.override.ini (site) and administrator/language/overrides/<tag>.override.ini
(administrator), one file for each language, and are edited under System > Manage > Language Overrides. The
loader merges the override file last, so it wins over the extension’s own file and over the default language; it
also reads the override file of the default language once for a non-default language, so that a key that exists only in
the default override does not fall back to a raw constant. The detail of the screen is in
Joomla Multilingual Sites.
; language/overrides/en-GB.override.ini
COM_TALLPINE_TRAIL_DISTANCE_LABEL="Route length (km)"
; language/overrides/de-DE.override.ini
COM_TALLPINE_TRAIL_DISTANCE_LABEL="Streckenlänge (km)"
For extension developers, the practical consequences are:
-
Do not edit the files of an installed extension to change a word. The next update replaces them.
-
A key that the code builds dynamically (for example
'COM_TALLPINE_DIFFICULTY_' . strtoupper($value)) works with overrides, but the override editor cannot search for it. Document the full list of generated keys for the site builder. -
An override can break a message that is used with
Text::sprintf()if it changes the placeholders. Keep every%sand%1$sof the original. -
Overrides are loaded for the whole site, so an override of a core key such as
JYESchanges it everywhere.
Finding missing and broken strings
To debug a translation open System > Global Configuration > System and set Debug Language to Yes. With Language display on Value the output marks the strings:
-
**text**(shown between asterisks) is translated from a language file -
??text??(shown between question marks) was requested but no file defines it -
text without a marker is not passed through a language function at all
The second kind is the one a developer looks for. A string without a marker is a hard-coded text in a layout. Enable the System - Debug plugin with Debug System on to get panel tabs that list the loaded language files and the constants that were requested but not found, with the file and line of the caller. The debug mode also checks the syntax of each file that is parsed: it reports lines whose quotes are unbalanced, lines that do not match the key pattern and the reserved keys. Keep the debug options off in production (see Debugging).
A practical check for a build pipeline is to compare the key sets of en-GB and de-DE files, and to lint each file with
php -r 'var_dump(parse_ini_file($argv[1], false, INI_SCANNER_RAW));' file.ini, which fails for unbalanced quotes. The
build and CI side is in Build and CI for Extensions.
The language cache (Joomla 6.0)
Parsing an INI file for every request is wasteful, because a front-end request loads tens of language files. Joomla 6.0 added a language file cache (pull request "Cache language files", merged in April 2025 as part of the 6.0 line). Two follow-up fixes in the 6.0 series improved its error handling: a corrupt cache file is detected and regenerated, and a failure to write the cache is logged instead of raising an exception.
How it works, from LanguageHelper::parseIniFile():
-
The first time a language file is loaded, the INI file is parsed and the resulting array is written to a PHP file in
JPATH_CACHE/language/(cachefor the site andadministrator/cachefor the administrator). The file name is made of the path of the source file relative to the site root with the slashes turned into dashes, then the modification time of the source file, and the extension.php. For example the file for the German component strings is named likelanguage-de-DE-com_tallpine.ini.<mtime>.php. -
The cache file is a small script: a guard
defined('_JEXEC') or die;andreturnof the array as PHP code. -
On later requests the cache file is loaded with
include. This avoids the INI parser, and as the file is plain PHP, OPcache keeps the array in memory too. -
When someone edits the INI file its modification time changes, the name of the cache file changes and a new file is written. Old cache files are not deleted, so a site that is updated often accumulates a few files per language file.
-
If the cache file does not return an array, it is deleted and the INI file is parsed again.
-
If the cache cannot be written (a read-only
cachefolder, a full disk), the strings are still returned from the parse and a warning is logged in thelanguagecategory.
This cache is independent of the System > Clear Cache storage (the Cache classes with file or Redis handlers) and of the
page cache. The other caches in the language area are separate too: the list of installed and content languages
(getInstalledLanguages(), getContentLanguages()) is cached in the com_languages output cache group. See
Caching and Performance for those.
Practical consequences:
-
The
cachefolder must be writable by the web server user, and on a read-only container filesystem it should be a writable volume. Otherwise every request re-parses the files and logs a warning. -
A deployment that replaces language files in place with the same modification time (some archive extractors and image builds preserve the original timestamps) can keep serving an older cache file if the timestamp is identical. Normal installs change the time and are not affected. After a deployment that touches language files, clear the folder
cache/languageor the Joomla cache. -
The cache needs a writable
cachefolder on every web node; with several nodes behind a load balancer each node fills its own folder, which is correct and needs no sharing. -
Older books and tutorials describe language files being parsed on every request and recommend turning off unused languages for speed. With the cache the effect is much smaller, but unused languages and extensions are still worth removing.
Packaging language packs
There are two things to package, and they are not the same.
Translations that belong to an extension
The normal case: the German strings of com_tallpine are part of the extension package. The files sit in the extension’s
folders as shown above and are listed in the manifest. One ZIP carries all languages, and the installer copies each
file into the right place. No separate installation is needed, and the translation is updated together with the extension.
This is the approach for the Tallpine package:
<?xml version="1.0" encoding="UTF-8"?>
<extension type="package" method="upgrade">
<name>pkg_tallpine</name>
<packagename>tallpine</packagename>
<version>1.0.0</version>
<files>
<file type="component" id="com_tallpine">com_tallpine.zip</file>
<file type="module" client="site" id="mod_tallpine_nexthikes">mod_tallpine_nexthikes.zip</file>
<file type="plugin" group="content" id="tallpine_conditions">plg_content_tallpine_conditions.zip</file>
</files>
</extension>
Source: Joomla Manual: Manifest files
Translations as a separate language pack
A language pack is an extension of type language. It is what the Install Languages screen downloads for the core
interface, and a club or an agency can make one for a third-party extension, or for a regional variant. The core English
pack describes the shape of its manifest: type="language", a client of site or administrator, and the language tag.
<?xml version="1.0" encoding="UTF-8"?>
<extension client="site" type="language" method="upgrade">
<name>Deutsch (de-DE) for Tallpine</name>
<tag>de-DE</tag>
<version>1.0.0</version>
<creationDate>2026-10</creationDate>
<author>Tallpine Hiking Club</author>
<description>German strings for the Tallpine package.</description>
<files>
<filename>com_tallpine.ini</filename>
<filename>mod_tallpine_nexthikes.ini</filename>
</files>
</extension>
A full language, like the German pack of the project, additionally contains the metadata file langmetadata.xml with the
fields the language manager reads: name, native name, tag, right-to-left flag, locale list, first day of the week, weekend
days and calendar. The English administrator file contains <tag>en-GB</tag>, <rtl>0</rtl>, <locale> and
<firstDay>0</firstDay>, <weekEnd>0,6</weekEnd>, <calendar>gregorian</calendar> in its <metadata> element. Older
packs use <tag>.xml (for example en-GB.xml) instead of langmetadata.xml, and the installer still looks for that name.
A pack for a language that needs special plural forms or transliteration also contains the localise.php file with the
static methods described above (getPluralSuffixes(), and the optional transliterator, ignored search words and search
length limits callbacks).
Practical points when packaging:
-
Use one ZIP per language for a standalone pack, or a
packagemanifest that contains several. The name of the pack should carry the language tag. -
Keep the key sets of all languages equal. A missing key falls back to the default language when debugging is off, but a missing placeholder or a wrong placeholder count breaks the page, as described above.
-
The extension version and the pack version should be tracked together; the update server of the extension can list the pack as a separate extension. See Update Servers and the JED.
-
Do not install the translation of a template or module into
language/overrides: overrides are site configuration, not distributable files. -
For the German strings of the core and of common extensions, point site builders to the official translation packs that the Install Languages screen offers, and distribute only your own extension’s strings.
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
A raw constant such as |
The key is missing from the file of the active language and of the default language, or the file was never loaded (a
plugin without |
The text appears in English on the German site |
The German file lacks the key and the default language filled it. Compare key sets, and check that the file really is in
|
A page fails only in one language |
A translation has more placeholders than the code passes, so |
Everything after a quote in a value is missing, or the whole file shows raw constants |
An unescaped double quote or an unbalanced quote makes |
The value |
A key named |
|
The text goes through |
A JavaScript string is empty |
The key was not registered with |
A changed |
An override exists for that key, or an older file is cached by OPcache or by a page cache. Clear the cache; on a deploy
that preserved file times, remove |
A Joomla 3 extension shows |
It uses the old quote token. Replace it with |
|
Use the language of the application ( |
Security considerations
-
Translations are trusted code-adjacent content, user data is not. The string in a language file may contain HTML, and Joomla does not escape it. A pack from an untrusted source can therefore inject markup (a stored script in every page that prints a constant). Install language packs only from the official installer list or from your own build, and review third-party packs before use.
-
Escape inserted values.
Text::sprintf()inserts values into the sentence without escaping them. Escape user-supplied parts first, or the translation becomes an XSS carrier. -
Never build a key from user input (
Text::_($_GET['label'])). It allows an attacker to display any registered string and, with the comma behaviour ofText::_(), to feed a format string. UseText::_()with fixed keys and map user choices through a whitelist. -
Do not leak data in JavaScript strings. Everything registered with
Text::script()is visible in the page source of every visitor. Never register a string that contains a token, a path or a private message. -
Debug output exposes paths. The language debug panel lists files and call locations; it must be off in production.
-
Override files are written by the administrator and are executable only as INI data, but they are loaded on every request, so restrict the Language Overrides permission to administrators.
Version notes and book corrections
| Version | Change |
|---|---|
Joomla 4.0 |
File names lose the language prefix ( |
Joomla 4.x to 5.x |
|
Joomla 5.0 |
Plugins use |
Joomla 6.0 |
Language files are cached as PHP files in |
Joomla 6.2 |
Language fallbacks: a language can name a fallback (in |
Joomla 7.0 (planned) |
Plugin properties |
Corrections to older material:
-
Developing Extensions for Joomla! 5 (2023) and Joomla! 4 Masterclass target Joomla 4 and 5. They still show
Factory::getLanguage()andJText::_()in places. In Joomla 6 useJoomla\CMS\Language\Textand the language of the application;JTextexists only with the backward-compatibility plugin. -
Joomla 3 tutorials use the
"_QQ_"token, language-prefixed file names andJText::script()with the old array; these are replaced by\", short file names andText::script($key). -
Older sources describe the language
.iniparsing as a per-request cost; Joomla 6.0 caches the parse as PHP files. -
The "language pack" and the "translation inside an extension" are different things: the first is an installable extension of type
language, the second is a set of files listed in<languages>of the extension’s manifest.