Translations and Language Files

9 views Markdown

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

Language
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;
Language::g() A key from a root language file. Returns the text, or false when any segment of the path is missing.
Language::gc() A key from a per-screen file under cm/. Use this in helpers, modules and exceptions, since it names its file rather than relying on what the current screen loaded.
Language::selected() The language code being served, such as 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::$init->rank_list() The installation's languages in display order. An instance method: there is no static Language::rank_list().

Which File Each Lookup Reads

CallReadsPath 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 filetheme/{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.

the file, then the call
// 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.

one row of each
// 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

Language, plurals
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;
plural() Picks the form for a count. Never choose it yourself with n === 1 — that holds only for two-form languages. Russian wants three forms, Arabic six.
plural_set() Every form as a map, for a count only the browser knows. The server sends the set and wcpPlural() chooses in the page.
plural_category() The category a number falls into: one, few, many, other and so on.

Forms With a Language Strip

Language, form scope
public static function form_langs(array $defined = []): array;
public static function posted_langs(): array;
public static function removed_langs(): array;
form_langs() The languages a record's strip opens with: the primary one, plus whatever that record already carries.
posted_langs() The languages the submitted form declared. Walk these when saving, not rank_list(). A language the operator never opened still posts empty values, and writing those over a stored translation erases it.
removed_langs() The languages the operator closed and confirmed. The primary language can never be one of them.

Where Text Lives

coremio/locale/{lang}/ Application text, one directory per language, split into packages that mirror the screens.
Module files Each module carries its own lang/ directory, loaded into $this->lang by the constructor. Its text travels with it rather than being added to the application's.
Theme files A theme carries its own wording too, reached through the theme/ prefix, so two themes can name the same thing differently.
Translated records Content an operator writes lives in the database, one row per language beside the record.

Example

looking text up
// 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"]];
the other side: writing the key
// 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

Replacements are second, the language is third

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.

A missing key answers false

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.

A nested key is not a key with a slash in it

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.

The first language list in a request fixes the shape for the rest

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.

Add the key to every language, not only yours

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.