Developers
Languages
Every string a module puts on the page should come from a translation file, so that a hub can change the wording without editing the module and so that other languages can be added later.
That second reason matters more for a module than for a component. A module sits in the furniture of every page, so its wording is exactly what a hub wants to adjust — "Upcoming bookings" here, "Your instrument time" there. If the string is in the layout, the hub has to fork the module to change it.
Where the files go
app/modules/mod_upcoming_bookings/
language/
en-GB/
en-GB.mod_upcoming_bookings.ini
en-GB.mod_upcoming_bookings.sys.ini
The directory is the language tag, and the file name is the tag, a dot, and
the module's full element name — prefix included. The .sys.ini file holds
the handful of strings the CMS itself needs before the module runs: the name
and description shown in the Module Manager and the installer. Everything the
module renders goes in the plain .ini.
The .sys.ini needs exactly two keys, and the first of them is not obvious:
MOD_UPCOMING_BOOKINGS="Upcoming bookings"
MOD_UPCOMING_BOOKINGS_XML_DESCRIPTION="Lists the current user's next instrument reservations."
The Module Manager's Select a Module Type screen runs the extension's
name — which addModuleEntry() set to the element, mod_upcoming_bookings
— through Lang::txt(), and the manifest's <description> through it as
well. So a key spelled like the element name is what gives the module a
readable title. Omit the file and the screen offers a module called
mod_upcoming_bookings described as
MOD_UPCOMING_BOOKINGS_XML_DESCRIPTION. Nothing errors; it just looks
unfinished.
When they are loaded
You do not load them. Hubzero\Module\Loader::render()
loads the module's language file immediately before including the entry file,
trying two locations and stopping at the first that produces strings:
app/bootstrap/{client}/language/{tag}/{tag}.mod_upcoming_bookings.ini{module directory}/language/{tag}/{tag}.mod_upcoming_bookings.ini
The first path is the hub's own override directory, where {client} is site
or administrator. A hub that wants to reword one string in a shipped module
puts a file there containing only that key; because the override is tried
first and loading stops on success, the file must then contain every key
the module uses, not just the changed one.
The translator also loads the default language before the requested one unless
debug_lang is set, so a key missing from a partial translation falls back to
en-GB rather than rendering raw.
Writing the file
; @package hubzero-cms
; @copyright Copyright (c) 2005-2020 The Regents of the University of California.
; @license http://opensource.org/licenses/MIT MIT
; Note : All ini files need to be saved as UTF-8 - No BOM
MOD_UPCOMING_BOOKINGS_HEADING="Upcoming bookings"
MOD_UPCOMING_BOOKINGS_NONE="You have no instrument time booked."
MOD_UPCOMING_BOOKINGS_SLOT="%s until %s"
MOD_UPCOMING_BOOKINGS_PARAM_LIMIT_LABEL="# to display"
MOD_UPCOMING_BOOKINGS_PARAM_LIMIT_DESC="The number of reservations to display."
MOD_UPCOMING_BOOKINGS_PARAM_INSTRUMENT_LABEL="Instrument"
MOD_UPCOMING_BOOKINGS_PARAM_INSTRUMENT_DESC="Limit to one instrument, or 0 for all."
Rules the parser enforces or the convention expects:
- Keys are uppercase, words separated by underscores, and start with
MOD_{MODULENAME}_so they cannot collide with another extension's. - Values are double-quoted. Files are parsed with
parse_ini_file()in raw mode, so a literal double quote inside a value is escaped as\". - Comments start with a semicolon.
- Keys used in the manifest — the
labelanddescriptionattributes on<field>, and the<description>element — are looked up in the same file, so parameter labels are translatable too.
Using a string
Lang::txt() takes a key and returns the translated string, passing any extra
arguments through sprintf:
<p><?php echo Lang::txt('MOD_UPCOMING_BOOKINGS_NONE'); ?></p>
<p><?php echo Lang::txt('MOD_UPCOMING_BOOKINGS_SLOT', $starts, $ends); ?></p>
with the placeholders in the value:
MOD_UPCOMING_BOOKINGS_SLOT="%s until %s"
A key with no translation is returned unchanged, which is how a missing string
shows up on the page as MOD_UPCOMING_BOOKINGS_SLOT in capitals rather than
as an error. That is the failure to watch for: capitals in the sidebar mean a
typo in the key, a stale cached copy of the module's output, or a file
whose name does not match the module element.
Registering the file
List the file in the manifest so the installer copies it:
<languages>
<language tag="en-GB">en-GB.mod_upcoming_bookings.ini</language>
</languages>
See Languages for how the language tag is chosen for a request and how additional languages are installed.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.