Skip to main content
Languages in Joomla
On this page
# Topics

Languages in Joomla

06 June 2026

The word "language" means two different things in Joomla, and people mix them up all the time. This article is about the first meaning: the language files that hold the text of the interface. It is not about publishing the same website in several languages.

This matters more than it sounds. Even a website that is 100% English is built completely on language files. They are not optional. They are how Joomla puts text on the screen at all.

The one idea to take home is this: Joomla never hardcodes user-facing text. The code refers to a key, and a separate file says what that key means in a given language. Swap the file, and the whole interface changes language without touching any code. Once you understand the KEY="value" contract and the order in which files load, you can translate, override, or extend any string on any Joomla site.

How language files hold every word of system text Joomla shows you.

This article starts with the basics for website owners and editors, moves on to the practical tools for administrators, and ends with the technical details for developers.

1. The Basics

1.1 Two Meanings of "Language"

Before anything else, let us separate the two concepts that share the same word:

TopicLanguage files (this article)Multilingual content (not this article)
What it is The translation files for the interface Publishing the same site in several languages
Scope Buttons, labels, messages, errors Articles, menus, modules per language
Powered by Text::_() and .ini files com_languages, Language Filter plugin, Associations
Who needs it Every developer and template builder Sites with a multilingual audience

We cover only the first column here.

1.2 Language Is an Extension Type

A language is one of Joomla's extension types. It is installed, enabled, updated, and uninstalled just like any other extension. The only difference is that it ships .ini files instead of PHP logic:

TypeRolePrefix
Component Main page content or application com_
Module Small boxes around the content mod_
Plugin Event-driven behaviour plg_
Template Look, feel, and page layout tpl_
Language Interface translations pkg_ / tag

1.3 The Language Tag

Every language has a unique tag in the form xx-XX: a lowercase ISO-639 language code, a hyphen, and an uppercase ISO-3166 region code.

en-GB   English (United Kingdom)   ← Joomla's source language
en-US   English (United States)
nl-NL   Dutch (Netherlands)
nl-BE   Dutch (Belgium)
de-DE   German (Germany)
ar-AA   Arabic                     ← right-to-left

The tag en-GB is special. It is the language Joomla is authored in, and it is the universal fallback. If a string is missing in nl-NL, Joomla shows the en-GB text instead.

1.4 Where the Files Live

There are two parallel trees, one for each "client" (front-end and back-end), exactly like templates:

public_html/
├── language/                     ← SITE (front-end) languages
│   ├── en-GB/
│   ├── nl-NL/
│   └── overrides/                ← your custom string overrides
└── administrator/
    └── language/                 ← ADMINISTRATOR (back-end) languages
        ├── en-GB/
        ├── nl-NL/
        └── overrides/

The front-end and back-end are translated independently. A visitor can see Dutch while you administer the site in English, because they load from different folders.

1.5 Inside a Language Folder

One language folder holds many files. There is one .ini file per extension, plus a few special members:

language/en-GB/
├── joomla.ini             ← shared/global strings (the "core dictionary")
├── lib_joomla.ini         ← framework library strings
├── com_content.ini        ← one file per component ...
├── com_content.sys.ini    ← ... plus its "system" strings (see section 3)
├── mod_articles_latest.ini
├── plg_system_...ini
├── langmetadata.xml       ← the manifest (tag, locale, rtl, calendar)
├── localise.php           ← per-language rules (plurals, transliteration)
└── index.html

One .ini per extension keeps things modular. Joomla loads only the files a page actually needs.

Back to top

2. The .ini File Format

2.1 The Contract: KEY="value"

A language file is a lookup table. Each line maps a constant to a piece of human-readable text:

; Comments start with a semicolon
; Note: all .ini files MUST be saved as UTF-8 (no BOM)

COM_CONTENT="Articles"
COM_CONTENT_SAVE="Save Article"
JGLOBAL_TITLE="Title"

The rules are strict and unforgiving:

RuleDetail
Encoding UTF-8, with no byte-order-mark (BOM)
Quotes The value is always in double quotes: KEY="value"
Keys UPPER_SNAKE_CASE, no spaces, prefixed with the extension by convention
One per line KEY="value" - no multi-line values
Comments Lines starting with ;

2.2 The Traps That Break a Whole File

This is important: a single malformed line can stop the entire file from loading. When that happens, every key in the file silently falls back to showing its raw constant on screen.

  • Reserved words. The words YES, NO, TRUE, FALSE, NULL, ON, OFF, and NONE are reserved in the INI specification. That is exactly why every value is quoted, and why Joomla ships JYES="Yes" instead of a bare value.
  • An unescaped double quote inside a value. To print a literal ", you must escape it:
; modern Joomla - backslash escape
COM_EXAMPLE_HELLO="Say \"Hello\" to the world"

; legacy placeholder still understood by older parsers
COM_EXAMPLE_HELLO_OLD="Say _QQ_Hello_QQ_ to the world"
  • No key on the left, a stray =, or an unquoted value will cause a parse error.

The practical tip: turn on Global Configuration → System → Debug Language while editing. Joomla then reports the error and the exact line number for any broken .ini file.

2.3 Values Can Carry HTML and Placeholders

The value is just text. It may contain HTML, entities, and printf-style placeholders such as %s and %d that the code fills in later (see section 7):

COM_USERS_REGISTER_SUCCESS="Thank you, <strong>%s</strong>, for registering."
JLIB_HTML_BATCH_NO_CATEGORY="- Keep original Category -"
Back to top

3. .ini vs .sys.ini

Every extension ships two language files, not one. This detail catches almost everyone out.

FileHoldsLoaded
com_content.ini The full runtime strings: every label, button, and message inside the component When the component actually runs
com_content.sys.ini A tiny set: the extension's name and description, its menu items, and install text Early and often: by the extension manager, menu builder, and module lists
com_content.sys.ini  (small, always cheap to load)
└── COM_CONTENT="Articles"
└── COM_CONTENT_XML_DESCRIPTION="The articles component ..."

com_content.ini      (large, only when the component renders)
└── COM_CONTENT_SAVE, COM_CONTENT_N_ITEMS_ARCHIVED, ... (hundreds)

Rule of thumb: if a string must appear before the extension runs - in the install screen, the extensions list, or the menu-type picker - it belongs in .sys.ini. Everything else goes in the main .ini.

Back to top

4. File Naming and Location

4.1 The Modern Convention (Core)

Core extension files live centrally and are named without a tag prefix. The folder name already tells Joomla the language, so the filename does not repeat it:

administrator/language/en-GB/com_content.ini
administrator/language/en-GB/com_content.sys.ini
language/en-GB/com_content.ini

4.2 The Legacy Prefixed Form (Still Valid)

Older and many third-party extensions use a tag-prefixed filename. The popular JCE editor still does this:

language/en-GB/en-GB.com_jce.ini
language/en-GB/en-GB.pkg_jce.sys.ini

Both forms work. Here they are side by side:

StyleExampleUsed by
Unprefixed (modern) com_content.ini Joomla 6 core
Prefixed (legacy) en-GB.com_jce.ini older / third-party extensions

4.3 Shipping Language Inside the Extension

A modern third-party extension does not have to install its files into the central tree at all. It can carry its own language/ folder, and Joomla loads it in place:

com_example/
├── com_example.xml
├── src/ ...
└── language/
    └── en-GB/
        ├── com_example.ini
        └── com_example.sys.ini

Keeping translations with the extension means an update ships its strings too. Nothing is left orphaned in /language/ after an uninstall.

Back to top

5. Anatomy of a Language Pack

A full language pack (for example nl-NL) is itself an installable extension. Beyond the .ini files, it has three special members.

5.1 langmetadata.xml - the Manifest

This file (called xx-XX.xml before Joomla 4) declares the locale to the whole system:

<metafile client="site">
    <name>English (en-GB)</name>
    <version>6.1.0</version>
    <metadata>
        <tag>en-GB</tag>
        <rtl>0</rtl>                       <!-- 1 = right-to-left -->
        <locale>en_GB.utf8, en_GB, english</locale>
        <firstDay>0</firstDay>             <!-- 0 = Sunday -->
        <weekEnd>0,6</weekEnd>
        <calendar>gregorian</calendar>
    </metadata>
</metafile>

This is what sets the locale for PHP's date and number functions, the right-to-left flag the template reads, and the first day of the week in calendar fields.

5.2 localise.php - Language-specific Rules

Some logic cannot live in a flat .ini file. Plural rules differ per language, so each pack may ship a small class:

abstract class En_GBLocalise
{
    // Which plural form applies for a given count?
    public static function getPluralSuffixes($count)
    {
        return $count == 1 ? ['1'] : ['MORE'];
    }
    // (some languages also define transliterate(), search-word rules, ...)
}

English has 2 plural forms, Polish has 3, and Arabic has 6. The method getPluralSuffixes() is how Text::plural() (section 7) knows which key to pick.

5.3 The overrides/ Folder

Each client has an overrides/ folder holding a single file, xx-XX.override.ini, that wins over everything else. This is the user-facing tool covered in section 10:

language/overrides/en-GB.override.ini
administrator/language/overrides/en-GB.override.ini
Back to top

6. Using Strings in Code

6.1 Text::_() - the Workhorse

You never echo a user-facing literal in code. You echo a key, and the Text class resolves it:

use Joomla\CMS\Language\Text;

echo Text::_('COM_CONTENT_SAVE');     // "Save Article"  (or "Artikel opslaan" in nl-NL)

The lookup works like this:

Text::_('COM_CONTENT_SAVE')
        |
        v
look up 'COM_CONTENT_SAVE' in the loaded language
        |
   +----+-------------+
 found?              missing?
   |                   |
 return value      return the KEY itself  (visible as a clue)

If you ever see COM_CONTENT_SAVE shown literally on screen, the key was not found. The cause is usually a wrong file, a file that did not load, or a typo.

6.2 The Text Family

The Text class has several static methods for different jobs:

CallUse
Text::_('KEY') Plain translation
Text::sprintf('KEY', $a, $b) Insert values into %s / %d
Text::plural('KEY', $n) Count-aware (1 item / N items)
Text::script('KEY') Make a string available to JavaScript
Text::alt('KEY', 'COMPONENT') A context-specific alternative key

These all live on Joomla\CMS\Language\Text. The old JText::_() is the same thing under the pre-namespace name.

6.3 Strings in XML - No PHP Needed

A large share of translatable text never touches PHP at all. Form fields, configuration options, menu types, and install manifests are XML, and Joomla translates their label and description attributes automatically. You just put the key there:

<!-- a config / form field: the attributes ARE language keys -->
<field
    name="title"
    type="text"
    label="COM_EXAMPLE_FIELD_TITLE_LABEL"
    description="COM_EXAMPLE_FIELD_TITLE_DESC"
/>
; ...resolved from your .ini exactly like Text::_()
COM_EXAMPLE_FIELD_TITLE_LABEL="Title"
COM_EXAMPLE_FIELD_TITLE_DESC="The headline shown at the top of the page."
Where the key appearsResolved by
label= / description= in config.xml, form .xml The form/field renderer
<menu>, <submenu> in the component manifest The menu builder (needs .sys.ini)
<name>, <description> in any manifest The extension manager (needs .sys.ini)

This is exactly why the .sys.ini file from section 3 must exist. The install screen, extensions list, and menu picker render those XML strings before the main .ini is ever loaded.

Back to top

7. Placeholders and Plurals

7.1 Text::sprintf() - Inserting Values

The .ini value carries the placeholders, and the code supplies the data:

COM_CONTENT_N_ITEMS_ARCHIVED="%d articles archived."
echo Text::sprintf('COM_CONTENT_N_ITEMS_ARCHIVED', $count);
// $count = 5  ->  "5 articles archived."

Keeping the number out of the string lets translators reorder the sentence. This is vital for languages where the verb or the number goes in a different place.

7.2 Text::plural() - Grammatically Correct Counts

"1 article" versus "5 articles" is not a simple if statement. Joomla builds the key from a base plus a count suffix, and localise.php decides the suffix:

COM_CONTENT_N_ITEMS_ARCHIVED="%d articles archived."   ; the default / MORE form
COM_CONTENT_N_ITEMS_ARCHIVED_1="Article archived."     ; the singular form
echo Text::plural('COM_CONTENT_N_ITEMS_ARCHIVED', $n);
//  $n = 1  -> key ..._1     -> "Article archived."
//  $n = 7  -> key ..._MORE  -> "7 articles archived."

The flow is:

Text::plural('..._ARCHIVED', $n)
        |
        v
getPluralSuffixes($n)  ->  ['1']  or  ['MORE']
        |
        v
try  ..._ARCHIVED_1  /  ..._ARCHIVED_MORE   (fall back to ..._ARCHIVED)

This is the single biggest reason a language is more than a word list. Grammar is code, and it lives in localise.php.

Back to top

8. Strings in JavaScript

The browser cannot read .ini files. The method Text::script() bridges the gap by pushing a key into a JavaScript dictionary that Joomla renders into the page:

Text::script('COM_CONTENT_CONFIRM_DELETE');   // PHP, during render
// later, in your script:
alert(Joomla.Text._('COM_CONTENT_CONFIRM_DELETE'));

Joomla collects every Text::script() key and outputs them as a Joomla.Text object. This keeps client-side code just as translatable as PHP.

Back to top

9. Loading and Load Order

9.1 Most Loading Is Automatic

When the dispatcher runs com_content, Joomla auto-loads com_content.ini for the active language and client. You rarely load files manually, but you can:

$lang = $this->getLanguage();              // or Factory::getApplication()->getLanguage()
$lang->load('com_content', JPATH_ADMINISTRATOR);   // force-load another extension's strings

9.2 The Cascade - Last Loaded Wins

Strings are merged into one big table as files load. A later definition overwrites an earlier one:

1. joomla.ini / lib_joomla.ini   (global dictionary)
2. com_xxx.ini                   (the component)
3. template / module strings
4. en-GB.override.ini            (OVERRIDES - always last, always win)
        |
        v
   one merged KEY -> value lookup table for this request

Because overrides load last, they beat core and extension strings. This is exactly what makes the override tool in section 10 safe and powerful.

9.3 Fallback to en-GB

When a string is missing, Joomla does not just give up:

Need 'COM_X_FOO' in nl-NL
   |
 in nl-NL.ini?  -- yes -> use it
   | no
 in en-GB.ini?  -- yes -> use English  (graceful fallback)
   | no
 show the KEY literally  (your cue that it is undefined everywhere)
Back to top

10. Language Overrides (No Files, No FTP)

10.1 The Built-in Tool

The screen System → Manage → Language Overrides lets you change any string - core or extension - straight from the admin, per language and per client:

System → Language Overrides
   ├── choose language + Site/Administrator
   ├── search the live constant (e.g. "COM_CONTENT_ARTICLES")
   └── save your replacement
        |
        v  writes to
language/overrides/en-GB.override.ini

Because overrides load last, your text wins. And because you never touched a core file, your change survives updates.

A classic use is to rename "Articles" to "News", or to soften an error message - site-wide, in seconds, and update-safe.

10.2 Debug Language - Find the Key Behind Any Text

Turn on Global Configuration → System → Debug Language. Joomla then wraps every string in markers so you can see what is happening:

  • **KEY** around a string means it was translated, and shows you its constant.
  • ??KEY?? means the key was not found (untranslated or missing).

This is how you discover the exact constant to put in an override. No guessing, and no searching through the filesystem.

Back to top

11. The Global Dictionary

11.1 joomla.ini - Strings Shared by Everyone

Generic, reusable strings live once in joomla.ini so that every extension can use them - and so that an override changes them everywhere at once:

ERROR="Error"
WARNING="Warning"
JYES="Yes"
JNO="No"
JALL="All"
JGLOBAL_TITLE="Title"
JACTION_CREATE="Create"
JACTION_DELETE="Delete"

The J prefix marks a global Joomla string. Reuse JGLOBAL_TITLE instead of inventing COM_MYTHING_TITLE - it is already translated in every pack.

11.2 Date and Number Formats Are Translatable Too

The file joomla.ini also localises how dates are formatted, using PHP date() patterns:

DATE_FORMAT_LC  = "l, d F Y"      ; Monday, 01 June 2026
DATE_FORMAT_LC2 = "l, d F Y H:i"  ; ... 14:30
DATE_FORMAT_LC4 = "Y-m-d"         ; 2026-06-01

A translator changes the month and day names in their .ini file and the order via these patterns. The date then renders naturally in every language without touching code.

Back to top

12. Locale, RTL, and Calendars

The metadata in langmetadata.xml (section 5.1) drives behaviour far beyond translation:

SettingEffect
<locale> Sets PHP's locale, for correct number, currency, and date formatting
<rtl> 1 flips the whole UI right-to-left; templates load *-rtl.css
<firstDay> First day of the week in every date picker (0 = Sunday, 1 = Monday)
<weekEnd> Which days are styled as weekend
<calendar> gregorian and others - the calendar field's system
Right-to-left languages (rtl=1): ar-AA (Arabic), he-IL (Hebrew), fa-IR (Persian)
   -> <html dir="rtl">, mirrored layout, template-rtl.min.css

Translation is words. Localisation is dates, plurals, reading direction, and calendars. A real language pack does both, and it is all declared in files, not coded.

Back to top

13. Language and the Database

13.1 The Big Surprise: the Strings Are Not in the Database

This trips up many administrators. Joomla's translations are file-based. There is no translations table. The .ini files on disk are the data. The database only records which languages exist and that they are installed as extensions:

TEXT (the words)         -> .ini files on disk          (NOT in the DB)
WHICH LANGUAGES EXIST    -> #__languages                (a few rows)
"a language is installed"-> #__extensions               (type='language')
OVERRIDES                -> overrides/xx-XX.override.ini (NOT in the DB)

13.2 The Three Tables That Do Participate

TableHolds
#__extensions One row per installed language pack: type='language', element = the tag (nl-NL), client_id (0 site / 1 admin), enabled, plus a manifest_cache snapshot of langmetadata.xml
#__languages One row per language known to the site: lang_code (en-GB), title, sef (en), image, published, access, ordering. This is what the language switcher and admin lists read
#__menu (indirect) Per-language home pages and assignments reference a language column - relevant once you go multilingual (out of scope here)
SELECT extension_id, element, client_id, enabled
FROM   #__extensions
WHERE  type = 'language';        -- every installed pack, site + admin

SELECT lang_code, title, sef, published
FROM   #__languages;             -- the languages the site knows about

13.3 Why There Is No Overrides Table

The Language Overrides tool from section 10 edits text, yet it writes nothing to the database. It appends to overrides/en-GB.override.ini on disk:

System → Language Overrides   --writes-->   language/overrides/en-GB.override.ini
                                              (a plain .ini, loaded last -> always wins)

Because overrides are files, they are easy to back up, to compare in git, and to deploy with the rest of the site. They also survive a database restore independently.

Back to top

14. Translating Joomla Itself

14.1 Installing a Language Pack

The screen System → Install → Languages lists every accredited translation team's pack and installs it from Joomla's update server - site and admin in one step:

System → Install → Languages
   -> pick "Nederlands (nl-NL)"  -> Install
   -> now selectable as Site/Admin default, and per-user

After install, set the defaults under System → Manage → Languages. Users can also pick their own admin language in their profile.

14.2 Where the Translations Come From

Core Joomla strings are translated by volunteer teams on a shared platform (historically Transifex, now Crowdin). The workflow looks like this:

en-GB .ini  (source strings authored by developers)
      |  uploaded
      v
   Crowdin   ← translation teams fill in every language
      |  built into packs
      v
Joomla update server  -> "Install Languages" in your site

When you add a new en-GB string to your own extension, that is the source translators work from. This is why clear keys and clean English text matter.

Back to top

15. Building and Best Practices

15.1 Adding Strings to Your Own Extension

The process for developers has three steps:

  1. Author every label as a key, never a literal: echo Text::_('COM_EXAMPLE_HEADING');
  2. Define it in your extension's language/en-GB/com_example.ini (plus .sys.ini for the name and menu).
  3. Ship those files inside the extension (section 4.3) so that updates carry them.
; com_example.ini
COM_EXAMPLE_HEADING="Welcome"
COM_EXAMPLE_N_RESULTS="%d results found"
COM_EXAMPLE_N_RESULTS_1="One result found"

15.2 Naming Keys Well

  • Prefix with the extension: COM_EXAMPLE_..., MOD_EXAMPLE_..., PLG_SYSTEM_EXAMPLE_.... This prevents collisions in the shared table.
  • Describe the string, not the value: use COM_EXAMPLE_SAVE_SUCCESS, not COM_EXAMPLE_GREEN_MESSAGE.
  • Reuse globals such as JYES, JGLOBAL_TITLE, and JACTION_* instead of duplicating them.
  • Keep keys stable. Renaming a key breaks every existing translation of it.

15.3 Where Should an Extension's Language Files Go?

This is a common question. The short answer: inside the extension. Ship them in the extension's own language/ folder. The central /language/ tree is a legacy approach and a site-owner escape hatch, not where you put them.

com_example/
├── com_example.xml
├── language/en-GB/com_example.ini  + .sys.ini      ← site strings
└── admin/language/en-GB/com_example.ini + .sys.ini ← admin strings
AspectInside the extension (recommended)Central /language/ (legacy)
Update New strings ship with the update Must copy files separately
Uninstall Removed cleanly Orphaned strings left behind
Packaging One self-contained zip Two things to track

The nuance: the central tree still wins if a file with the same name exists there - by design, so that a site owner can override your strings without editing your extension. But the cleaner tool for that is Language Overrides. So: you (developer) ship inside the extension; a site owner customising wording uses Language Overrides.

Back to top

16. Common Mistakes and Pitfalls

  • Hardcoding text with echo "Save"; instead of Text::_('..._SAVE') makes it untranslatable forever.
  • Saving an .ini as UTF-8 with BOM, or in Latin-1, means the file will not load.
  • A bare reserved word or an unescaped quote makes the whole file fail silently.
  • Putting runtime strings in .sys.ini bloats early loads; putting the extension name in .ini means it is missing from the extension and menu lists.
  • Editing core .ini files to change text gets wiped on update. Use Language Overrides instead.
  • Building plural text by hand with $n == 1 ? ... is wrong in most languages. Use Text::plural().
  • Forgetting Text::script() leaves JavaScript showing raw keys or English only.
  • Putting a field label key only in the main .ini makes it show raw in the config screen, which loads .sys.ini.
  • Hunting for an "overrides" database table is a dead end - there isn't one; overrides are files.
Back to top

17. Quick Reference

TRANSLATE     echo Text::_('KEY')
INSERT VALUE  echo Text::sprintf('KEY', $count)
COUNT         echo Text::plural('KEY', $n)   (+ key_1, key_MORE)
JAVASCRIPT    Text::script('KEY')  ->  Joomla.Text._('KEY')
XML FIELD     label="KEY" description="KEY"   (no PHP needed)
NAME/MENU     put the early strings in .sys.ini
GLOBAL        reuse JYES, JGLOBAL_TITLE, JACTION_*
OVERRIDE      System -> Language Overrides (writes overrides/xx-XX.override.ini)
FIND THE KEY  Global Config -> System -> Debug Language
INSTALL LANG  System -> Install -> Languages
FALLBACK      missing nl-NL string -> en-GB -> raw KEY
Back to top

18. Summary

In Joomla, language files are not a feature for multilingual sites only. They are the mechanism by which Joomla puts any text on the screen at all. They touch almost every layer of the system:

  • Interface: every button, label, message, and error comes from a .ini file.
  • Code: Text::_(), sprintf(), plural(), and script() keep strings out of the logic.
  • Configuration: XML label= and description= keys are translated for free.
  • Localisation: dates, plurals, reading direction, and calendars, all declared in the pack.
  • Maintenance: Language Overrides change any wording update-safely, as plain files you can back up and deploy.
  • Architecture: the translations live in files, not the database, which makes them portable and version-friendly.

Once you understand the KEY="value" contract, the difference between .ini and .sys.ini, the load cascade, and the override tool, you can translate, customise, or extend the wording of any Joomla site with confidence.

If you are building an extension, planning a translation, or staring at a screen full of raw COM_SOMETHING constants and wondering why, it pays to understand this layer early. Language files are the quiet foundation under every word Joomla shows.

Back to top
Languages in Joomla
Peter Martin

Joomla en Linux specialist voor snelle, veilige en schaalbare websites.