Developers
Languages
Every string a template prints should come from a language file, so the
template can be translated without being edited. A template's strings live in
its own language directory and are loaded for you.
Why bother
Two reasons, and the second is the one that catches people.
A template is the part of the hub that prints the most bare English: Log in,
Search, Skip to main content, Help. If those are literals in index.php,
the hub cannot be translated no matter what anyone does to the components.
More immediately, a hub that installs your template can replace any string in
it without editing your files — by dropping a file into its own
app/bootstrap/site/language/ directory. That is the supported way for one hub
to say Reserve where northgate says Book. A hardcoded string forces a
fork of the template.
Where the files go
app/templates/northgate/
language/
en-GB/
en-GB.tpl_northgate.ini
The directory is language, singular. Inside it, one directory per language
tag, and inside that a file named {tag}.tpl_{template}.ini. The tpl_
prefix and the .ini extension are both required:
Translator::load()
builds the filename as {basePath}/language/{tag}/{tag}.{extension}.ini, and
the extension it is given for a template is tpl_ plus the template's
directory name.
All four site templates and kameleon ship one. Nothing else in a template
tree is scanned for strings.
Writing one
A language file is key/value pairs. Keys are case-insensitive — they are
upper-cased before lookup — but the convention is upper case throughout, words
separated by underscores, prefixed TPL_{TEMPLATE}_:
; @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
TPL_WELCOME="Welcome template"
TPL_WELCOME_XML_DESCRIPTION="HUBzero welcome template"
; Parameters
TPL_WELCOME_FIELD_FLAVOR_LABEL="Service"
TPL_WELCOME_FIELD_FLAVOR_DESC="Display for use on the specified service. This will typically add an icon."
TPL_WELCOME_FIELD_FLAVOR_NONE="(none)"
TPL_WELCOME_FIELD_TEMPLATE_LABEL="Template"
TPL_WELCOME_FIELD_TEMPLATE_DESC="The template to switch to with one-button-press."
; Congratulations
TPL_WELCOME_CONGRATS="Congratulations, you are now running HUBzero %s."
TPL_WELCOME_CONGRATS_AMAZON="Congratulations, you are now running HUBzero %s in the Amazon Cloud."
The prefix is not enforced. It exists to keep one extension's keys from
colliding with another's, because every loaded file merges into one flat table
of strings. Two extensions that both define TITLE will fight over it.
Files must be UTF-8 with no byte order mark.
Loading
You do not need to load the template's own file. The document loads it while fetching the template:
// Load the language file for the template
$lang = \App::get('language');
$lang->load('tpl_' . $template, PATH_APP . DS . 'bootstrap' . DS . \App::get('client')->name, null, false, true) ||
$lang->load('tpl_' . $template, $directory . DS . $template, null, false, true);
Note the pair of calls. The first looks under
app/bootstrap/{client}/language/{tag}/, which is where a hub puts a
replacement for a shipped template's strings; the second falls back to the
template's own directory. Whichever loads first wins, so a hub can override
individual templates' strings without touching core/.
To load some other extension's strings, call Lang::load() yourself. A
template layout that prints a com_bookings string needs this, because nothing
loads a component's language file for a page that is not running that
component:
<?php
defined('_HZEXEC_') or die();
// From app/bootstrap/site/language/en-GB/en-GB.com_bookings.ini
Lang::load('com_bookings');
// From a specific directory
Lang::load('com_bookings', PATH_APP . '/components/com_bookings/site');
The signature is
load($extension = 'hubzero', $basePath = PATH_APP, $lang = null, $reload = false, $default = true).
PATH_APP and PATH_CORE are special-cased: passing either on its own means
{path}/bootstrap/{client}, not the path itself. Any other base path is used
as given, with /language/{tag} appended.
$default — true by default — makes the loader fall back to en-GB when the
current language has no file of its own.
Printing a string
<p><?php echo Lang::txt('TPL_NORTHGATE_BOOK_AN_INSTRUMENT'); ?></p>
Extra arguments are passed through sprintf, so a file can hold format
strings:
TPL_WELCOME_CONGRATS="Congratulations, you are now running HUBzero %s."
echo Lang::txt('TPL_WELCOME_CONGRATS', $version);
What failure looks like
A key with no translation is returned unchanged. There is no error and no blank
space: the page shows the literal TPL_NORTHGATE_BOOK_AN_INSTRUMENT where the
words should be. Every cause produces the same symptom —
- the file is named for the template it was copied from;
- the key is defined but the file is in
languages/rather thanlanguage/; - the key is misspelled in the layout;
- the string is a component's and nobody called
Lang::load().
Turn on Debug Language in Global Configuration to tell them apart: found
strings are wrapped in **asterisks** and missing ones in ??question marks??.
A key with no asterisks and no question marks is not being looked up at all.
Strings the administrator sees
Two things outside the template itself are translated, and they do not load the same file.
Parameter labels. The label and description attributes in the
<config> block of templateDetails.xml are language keys.
com_templates
loads the template's main tpl_{template} file before it builds the
style-editing form, so TPL_NORTHGATE_FIELD_HEADER_LABEL resolves there.
Module position names. When
com_modules
builds the position list it reads <positions> from every installed template's
manifest and, for each, loads only tpl_{template}.sys — the .sys.ini file,
never the main one. It then looks for a key named
TPL_{TEMPLATE}_POSITION_{POSITION}, upper-cased, and falls back to
COM_MODULES_POSITION_{POSITION} from its own language file if the template's
key is not defined.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.