Framework
Features

Internationalization

Every user-facing string lives in a per-language JSON file and is read through NLS, so the same code renders in whatever language the request is in.

Configuration

IntlConfig is where the layer is set up. In a config file it sets the default language and points at your copy directories — strings, plus the email and notification content:

config/Intl.config.php
use Framework\Intl\IntlConfig;

IntlConfig::setDefaultLanguage("en");
IntlConfig::setStringsDir("nls/strings");
IntlConfig::setEmailsDir("nls/emails");
IntlConfig::setNotificationsDir("nls/notifications");

The defaults are those same nls/… paths, so you only call the setters to move them. The default language is the fallback used whenever no language is set on the request.

The string files

Strings live under nls/strings, one JSON file per language named by its code. Keys are shared across languages — you translate the values:

nls/strings/en.json
{
    "GENERAL_ACCEPT":  "Accept",
    "PRODUCT_CREATED": "The product was created",
    "PRODUCT_COUNT_SINGULAR": "{0} product",
    "PRODUCT_COUNT_PLURAL":   "{0} products"
}
nls/strings/es.json
{
    "GENERAL_ACCEPT":  "Aceptar",
    "PRODUCT_CREATED": "El producto fue creado",
    "PRODUCT_COUNT_SINGULAR": "{0} producto",
    "PRODUCT_COUNT_PLURAL":   "{0} productos"
}

The Language class

Every language file found is discovered at build time and turned into a typed Language class, so the set of languages your app supports is known at compile time rather than hard-coded in a list:

use Framework\System\Language;

Language::getAll();          // [ "en" => "English", "es" => "Español" ]
Language::getRootCode();     // the default language code
Language::isValid("es");     // true
Language::getCode("es");     // the stored code for a language
Language::getSelect();       // a Select list, ready for a dropdown

getSelect() is the quickest way to render a language picker; feed the chosen code back into the credential with Credential::setLanguage() and every later request follows it.

The current language

You normally never set the language yourself. When a request is authenticated, Auth reads the language stored on the signed-in credential and calls NLS::setLanguage() with it — so from that point on every string resolves in the user's language automatically. While an admin is signed in as another user, the admin's language wins, so the interface stays in the language they read. With no user, the configured default applies.

use Framework\Intl\NLS;

NLS::getLanguage();       // the language in effect for this request
NLS::setLanguage("es");   // override it — rarely needed

The catch is that this only happens when there is a user. An API request authenticates with a static token and carries no credential, and the same is true of a console command or a cron run — so nothing sets a language and every string falls back to the configured default. In those contexts pass the language explicitly, either per call or once for the whole run:

// Per call — the second argument of every read method
NLS::getString("PRODUCT_CREATED", $language);
NLS::format("HOME_WELCOME", [ $name ], $language);

// …or set it once, from whatever the API request carries
NLS::setLanguage($request->getString("language"));
A credential request sends its language as xLangcode, which Framework::execute() applies for you. An API request has no such step — decide where the language comes from (a request field, the account you are acting for) and set it yourself.

Reading strings

NLS is the class you read through — every method is static and takes an optional language as its last argument. NLS::getString() resolves a key in the current language, so most calls take just the key:

use Framework\Intl\NLS;

NLS::getString("GENERAL_ACCEPT");        // "Accept"
NLS::getString("GENERAL_ACCEPT", "es");  // force a language

Keys can hold more than a single string:

MethodReturns
NLS::getString()One translated string.
NLS::getList()A key holding an array of strings.
NLS::getIndex()One entry of such a list — how enums resolve their labels.
NLS::getMap()A key holding a map of values.
NLS::getSelect()A list turned into Select options.
NLS::getAll()The raw value behind a key.

Placeholders

Strings interpolate with numbered {0} placeholders, filled in order by NLS::format(). Numbering them means a translation can reorder the values when its grammar needs a different order:

// en: "Welcome back, {0}. You have {1} messages."
// es: "Tenés {1} mensajes, {0}."
NLS::format("HOME_WELCOME", [ $name, $count ]);

Two more helpers finish a value before it reaches a string — NLS::toYesNo() renders a boolean in the user's language, and NLS::formatNumber() formats a number for it:

NLS::toYesNo(true);            // "Yes" — "Sí" in Spanish
NLS::formatNumber(1234.5);     // the number in the language's format

// …then drop them into a string
NLS::format("ORDER_SUMMARY", [ NLS::formatNumber($total), NLS::toYesNo($isPaid) ]);

Plurals

Declare a _SINGULAR and a _PLURAL key, and NLS::pluralize() picks between them by count — passing the count in as {0}, so you never build the sentence by hand:

// "PRODUCT_COUNT_SINGULAR": "{0} product"
// "PRODUCT_COUNT_PLURAL":   "{0} products"

NLS::pluralize("PRODUCT_COUNT", 1);   // "1 product"
NLS::pluralize("PRODUCT_COUNT", 7);   // "7 products"

When the count comes from a list you already have, NLS::pluralizeList() takes the list itself and uses its size, passing the joined values in as the placeholder:

// "TAG_ADDED_SINGULAR": "Added the tag {0}"
// "TAG_ADDED_PLURAL":   "Added the tags {0}"

NLS::pluralizeList("TAG_ADDED", [ "New" ]);                 // "Added the tag New"
NLS::pluralizeList("TAG_ADDED", [ "New", "Hot", "Sale" ]);  // "Added the tags New, Hot and Sale"

Joining values

Listing values in a sentence is language-specific — the separator and the final conjunction change. NLS::join() builds that list for you:

NLS::join([ "New", "Hot", "Sale" ]);            // "New, Hot and Sale"
NLS::joinWithAnd([ "New", "Hot" ]);             // "New and Hot"
NLS::joinWithOr([ "New", "Hot" ]);              // "New or Hot"

To put that list inside a string, NLS::formatJoin() does both steps at once — it joins the values and drops the result into the key as {0}:

// "FILTER_APPLIED": "Filtering by {0}"

NLS::formatJoin("FILTER_APPLIED", [ "New", "Hot", "Sale" ]);
// "Filtering by New, Hot and Sale"

NLS::formatJoin("FILTER_APPLIED", $tags, useOr: true);
// "Filtering by New, Hot or Sale"

Localized urls

NLS::url() and NLS::urlPath() build a url whose segments are themselves translated keys, so a link reads naturally in each language:

NLS::url([ "URL_PRODUCTS", "URL_NEW" ]);   // /productos/nuevo in Spanish