Developers
Languages
Every string a component shows a person comes out of an .ini file through
Lang::txt(). No component in the tree hard-codes display text, and neither
should yours: a hub can override any string without touching your code, and a
translator can supply another language without reading it.
The habit costs nothing while you are writing and is expensive to add later, because retrofitting it means finding every sentence in every layout.
One file per client
Language files belong to a client, and the client that is running loads only its own. This is the detail that catches people:
app/components/com_bookings/
site/language/en-GB/
en-GB.com_bookings.ini the site loads this, and only this
admin/language/en-GB/
en-GB.com_bookings.ini the administrator loads this
en-GB.com_bookings.sys.ini and this, for lists of extensions
The name is {tag}.{extension}.ini inside a directory named for the tag. The
loader looks in {client directory}/language/{tag}/, so the path is fixed
once the client is.
Duplicating a handful of strings is the cost of the split. It buys a site that does not carry the administrator's vocabulary into every page it renders.
The format
Key/value pairs, one per line, values in double quotes. Lines beginning with a semicolon are comments:
COM_KB="Knowledge Base"
COM_KB_LOGIN_NOTICE="Please login to continue."
COM_KB_HELPFUL="helpful"
COM_KB_NOT_HELPFUL="not helpful"
COM_KB_CATEGORIES="Categories"
COM_KB_SUBCATEGORIES="Sub Categories"
COM_KB_MOST_POPULAR_ARTICLES="Most Popular Articles"
COM_KB_MOST_RECENT_ARTICLES="Most Recent Articles"
COM_KB_ANSWERS="Answers"
COM_KB_REPORT_PROBLEMS="Report Problems"
COM_KB_ARTICLES="Articles"
COM_KB_NO_ARTICLES_FOR_CATEGORY="There are currently no articles in this category."
COM_KB_NO_ARTICLES="No articles found."
COM_KB_DETAILED_QUESTION="Detailed question"
COM_KB_YOU_FOUND_THIS_ARTICLE="You found this article"
COM_KB_WAS_THIS_HELPFUL="Was this helpful to you?"
COM_KB_YES="Yes"
COM_KB_NO="no"
COM_KB_READ_ARTICLE="Read the full article"
COM_KB_MY_SUPPORT_TICKETS="My Support Tickets"
COM_KB_FOUND_THIS_HELPFUL="%s out of %s people found this helpful."
Keys may contain letters, digits, and underscores, but no spaces. Convention is
uppercase, words separated by underscores, prefixed with the component name:
COM_BOOKINGS_INSTRUMENT_UNAVAILABLE. Nothing enforces the prefix, but every
language file loaded into a request shares one flat table of strings, so an
unprefixed SEARCH will collide with somebody else's — and the collision
appears only on the pages where both are loaded. See
Languages.
Translating
echo Lang::txt('COM_BOOKINGS_INSTRUMENTS');
A key that is not defined is returned unchanged, which is why a missing translation shows up as a screaming uppercase key rather than a blank space. Nothing is logged, so the only way to find the ones you have not exercised is to look:
php tools/lint/undefined-language-keys.php app/components/com_bookings
Any further arguments are passed to sprintf(), so a string can carry
placeholders:
COM_BOOKINGS_SLOT_TAKEN="%s is already booked from %s until %s."
echo Lang::txt('COM_BOOKINGS_SLOT_TAKEN', $instrument->title, $from, $until);
Number the placeholders — %1$s, %2$s — whenever a translator might need to
reorder them. Getting the count wrong is a PHP warning from sprintf() and a
sentence with a hole in it, not an exception.
When files are loaded
Component::render() loads two files before your entry point runs, in this
order:
{client directory}/language/{tag}/{tag}.com_bookings.ini— the component's own strings, from the client that is running.PATH_APP/bootstrap/{client}/language/{tag}/{tag}.com_bookings.ini— the hub's overrides.
The second is loaded after the first, so a hub can redefine any single string
by putting just that key in its own file. Nothing is copied and nothing needs
to be complete. That is the supported way for a hub to reword your component;
tell hub administrators about it rather than letting them edit your .ini,
which an update overwrites.
SiteController's constructor loads the component's file from the client
directory as well, but only if nothing has loaded it yet — which covers a
controller instantiated outside the normal render path, from a module or a
plugin.
Requesting a non-default language loads the default language first, so a partial translation falls back to English string by string rather than showing raw keys.
The system file
{tag}.com_bookings.sys.ini in the administrator language directory holds
the strings the platform needs when your component is not the one running: its
name in the Extensions manager, and the labels for the menu item types it
offers. Leave it out and your component appears in the administrator's lists
as COM_BOOKINGS.
COM_KB="Knowledge Base"
; Views
COM_KB_ARTICLES_VIEW_DEFAULT_TITLE="Intro"
COM_KB_ARTICLES_VIEW_DEFAULT_OPTION="Intro"
COM_KB_ARTICLES_VIEW_DEFAULT_DESC="Knowledge Base landing page"
COM_KB_ARTICLES_VIEW_ARTICLE_TITLE="Article"
COM_KB_ARTICLES_VIEW_ARTICLE_OPTION="Article"
COM_KB_ARTICLES_VIEW_ARTICLE_DESC="Display a single article"
COM_KB_ARTICLES_VIEW_CATEGORY_TITLE="Category"
COM_KB_ARTICLES_VIEW_CATEGORY_OPTION="Category"
COM_KB is the component's name wherever the platform lists extensions. The
COM_KB_ARTICLES_VIEW_DEFAULT_TITLE form is what the menu manager falls back
to when a view offers no layout metadata: {COMPONENT}_{VIEW}_VIEW_DEFAULT_TITLE.
When the layout does have an .xml file beside it, its title attribute is
used instead — and that attribute is itself passed through Lang::txt(), so it
may be a key. See Views.
Adding a language
A translation is the same tree with a different tag: fr-FR/fr-FR.com_bookings.ini
beside the en-GB directory. The tag is a language and a region joined by a
hyphen; the path is built from it literally, so fr_FR produces a file
nothing looks for. The general mechanics — the tag list, the .sys.ini
convention, plural rules — are covered in
Languages and
Languages.
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.