Translations and Language Files
Nothing a person reads is written in code. Text lives in language files, is looked up by key, and a missing key answers false rather than the text you expected.
Overview
An installation serves more than one language at once, chosen per visitor. A literal string in a condition, an exception or a template is not a shortcut. It is text one group of users will never see in their own language, and that no translator can reach.
Two lookups cover everything: one for the root language files, one for the per-screen files under cm/. Both walk a slash separated path into nested arrays.
Reference
public static function g($key = '', $replaces = [], $slang = ''): string|int|array|bool;
public static function gc($name = '', $replaces = [], $slang = ''): string|int|array|bool;
public static function selected(): string;
// Instance, reached through the singleton: Language::$init->rank_list()
public function rank_list($status = 'active'): array;
// The instance methods the two static helpers delegate to. Note the argument
// order: get() puts the language second, get_cm() puts the replacements second.
public function get($arg = null, $slang = '', $replaces = []): string|int|array|bool;
public function get_cm($arg = null, $replaces = [], $slang = ''): string|int|array|bool;
false when any segment of the path is missing.
cm/. Use this in helpers, modules and exceptions, since it names its file rather than relying on what the current screen loaded.
en. Falls back to the installation's own language when the language layer is not up yet. That makes it safe on the command line.
Language::rank_list().
Which File Each Lookup Reads
| Call | Reads | Path shape |
|---|---|---|
Language::g("needs/button-save") | Root language files: needs, date, errors, actions, package | {file}/{key}/{subkey} |
Language::gc("admin/services/page-list") | Per-screen files under cm/, grouped by area | {area}/{file}/{key}, area being admin, website or system |
Language::gc("theme/hero-title") | The active theme's own language file | theme/{key} |
Every slash is one array level, not part of a name. A flat key written as 'job-title/daily' is never found; it has to be a nested array.
Placeholders and the Replacement Map
The second argument maps the literal placeholder text, braces or colon included, to its replacement. It is a plain string replacement, so the keys must match the file byte for byte.
// coremio/locale/en/cm/admin/departments.php
return [
'error1' => 'Name is required for {lang}.',
// The content/variables shape declares which placeholders exist. Lookups
// return the content string, never the wrapper.
'welcome' => [
'content' => 'Hello {name}, you have {count} messages.',
'variables' => '{name},{count}',
],
];
// The call. Keys carry their own braces; ':name' style keys work the same way.
$msg = Language::gc("admin/departments/error1", ['{lang}' => "TR"]); // "Name is required for TR."
$hi = Language::gc("admin/departments/welcome", ['{name}' => $name, '{count}' => 3]);
// Third argument forces one language instead of the one being served.
$tr = Language::gc("admin/departments/error1", ['{lang}' => "TR"], "tr");
What the Language List Returns
The row shape depends on the argument, so the two cannot be swapped. active returns display rows built for a switcher. all returns the raw package record of every installed language, including the disabled ones.
// Language::$init->rank_list() enabled languages, ordered by rank
[
'rank' => 2, 'local' => 0, 'selected' => true, 'key' => 'en',
'name' => 'English', 'global-name' => 'English',
'link' => 'https://example.test/en/services', // this page in that language
'cc' => '', 'cname' => 'Worldwide', 'pc' => 1,
'flag-img' => 'https://example.test/resources/assets/images/flags/en.svg',
];
// Language::$init->rank_list("all") every installed language, package record
// The whole record, in this order. 'key' and 'country-name' are added by the call;
// the rest is the package file as it sits on disk.
[
'create-date' => '2018-06-21 10:15:48',
'name' => 'English', 'show-name' => 'English',
'country-id' => 0, 'country-code' => '',
'code' => 'en', 'code-hyphen' => 'en_US',
'scharacters' => '', 'charset-code' => 'UTF-8',
'phone-code' => 1, 'currency' => 1, 'rank' => 2,
'permalink' => true, 'prefix' => 'enabled', 'status' => true,
'local' => false, 'rtl' => false,
'key' => 'en', 'country-name' => 'Worldwide',
];
Text That Carries a Number
public static function plural(string $key, int|float $n, array $replaces = [], string $slang = ''): string;
public static function plural_set(string $key, string $slang = ''): array;
public static function plural_category(int|float $n, string $slang = ''): string;
n === 1 — that holds only for two-form languages. Russian wants three forms, Arabic six.
wcpPlural() chooses in the page.
one, few, many, other and so on.
Forms With a Language Strip
public static function form_langs(array $defined = []): array;
public static function posted_langs(): array;
public static function removed_langs(): array;
rank_list(). A language the operator never opened still posts empty values, and writing those over a stored translation erases it.
Where Text Lives
lang/ directory, loaded into $this->lang by the constructor. Its text travels with it rather than being added to the application's.
theme/ prefix, so two themes can name the same thing differently.
Example
// Inside a screen whose package is already loaded.
$title = Language::g("needs/button-save");
// Anywhere else: name the file, then the key.
throw new Exception(Language::gc("admin/services/error-not-found"));
// Joining a translation table for the language being served.
$lang = Language::selected();
$rows = WDB::select("t1.id, COALESCE(t2.name, t1.name) AS name")
->from("products AS t1")
->join("LEFT", "products_lang AS t2", "t2.owner_id=t1.id AND t2.lang='" . $lang . "'")
->build();
// A language switcher.
foreach (Language::$init->rank_list() as $l)
$items[] = ['label' => $l["name"], 'href' => $l["link"], 'on' => $l["selected"]];
// coremio/locale/en/cm/admin/services.php and the same key in every other language
return [
'page-list' => 'Services',
'error-not-found' => 'Service not found.',
// Nested, so the path is admin/services/errors/timeout
'errors' => [
'timeout' => 'The provider timed out.',
],
];
// Written back from code when a screen edits a language file.
Language::save("admin/services", $data, "en");
Pitfalls
Both static helpers take the replacement map in position two and the language code in position three. The instance method behind g() reverses them. A snippet copied from inside the class puts a language code where a map belongs. Passing the language second to the static call silently produces no substitution and no error.
Not an empty string, and not the key itself. Written as Language::gc("...") ?: "Some text" the fallback hides the miss forever. The key looks alive while the language file never had it. Check a doubtful key on the command line and expect a string, not a boolean.
The path form walks into nested arrays, one level per slash. A key defined flat as 'meta-detail/title' is unreachable, and searching for the last segment alone finds the wrong entry or nothing.
Both arguments share one cached list and all overwrites it. Once anything in the request has asked for all, every later rank_list() answers with package records instead of display rows. link, selected, global-name and flag-img are absent, and the enabled-only filter is gone with them. A switcher shown after an administrative screen on the same request is where this appears. Each missing key is a logged warning rather than an error. Take the display rows before anything asks for all, or keep your own copy of them.
Adding text is a change to every language file the installation ships. Records in the database behave differently. A row with no translation for the active language shows its own value instead. That is why the two are handled by different code.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.