Differences from Laravel
@tolki/lang follows Laravel's Translator and MessageSelector wherever JavaScript can hold the same data. These are the places where it cannot, or chooses not to.
One object for the JSON and group files
Laravel loads lang/en.json and the lang/en/*.php files separately. This port takes one object per locale, in which a top-level array or plain object is a group and any other value is a JSON line. The key "*" reads the JSON lines as a group, so get("*.Hello") reads the JSON line Hello, as in Laravel.
import { Translator } from "@tolki/lang";
const translator = new Translator(
{ en: { Hello: "Hola", messages: { welcome: "Bienvenido" } } },
"en",
);
translator.get("*.Hello");
// => "Hola"
translator.get("messages.welcome");
// => "Bienvenido"This merged shape changes three results of addLines():
addLines({ "*.x.y": "xy" }, "en")stores one JSON line with the keyx.y. Laravel splits the rest of the key on dots, so it answers the keyx.ywithx.yand never finds the line.jsimport { Translator } from "@tolki/lang"; const translator = new Translator({ en: {} }, "en"); translator.addLines({ "*.x.y": "xy" }, "en"); translator.get("x.y"); // => "xy"addLines()drops a non-numeric item written into a group that is a list. Laravel keeps it under that key, next to the list's own indexes. A list stays a list here, as it does in@tolki/collection.jsimport { Translator } from "@tolki/lang"; const translator = new Translator({ en: { g: ["a", "b"] } }, "en"); translator.addLines({ "g.x": "X" }, "en"); translator.get("g"); // => ["a", "b"] translator.addLines({ "g.1": "B" }, "en"); translator.get("g"); // => ["a", "B"]addLines()replaces a JSON line with a group when both share a key, because one object cannot hold both. Laravel keeps both: afteraddLines({ "Hello.x": "X" }, "en")it still answersget("Hello")with the JSON line.jsimport { Translator } from "@tolki/lang"; const translator = new Translator({ en: { Hello: "Hola" } }, "en"); translator.addLines({ "Hello.x": "X" }, "en"); translator.get("Hello"); // => { x: "X" }
setLoaded() takes the same per-locale object as the constructor. Laravel's setLoaded() takes its loaded array, keyed by namespace, then group, then locale, such as ['*' => ['messages' => ['en' => [...]]]].
import { Translator } from "@tolki/lang";
const translator = new Translator({ en: { g: { a: "old" } } }, "en");
translator.setLoaded({ en: { g: { a: "new" } } });
translator.get("g.a");
// => "new"Types follow the constructor's lines
The keys and results are typed from the lines you pass to the constructor. A group that only addLines() added is typed as a string, the answer for a key with no line, even though get() answers the group. Read such a group with array(), which is typed as a group, or cast the result.
import { Translator } from "@tolki/lang";
const translator = new Translator({ en: { messages: { hi: "Hi" } } }, "en");
translator.addLines({ "extra.bye": "Bye" }, "en");
translator.get("extra");
// => { bye: "Bye" }
translator.array("extra");
// => { bye: "Bye" }Locales and the fallback
Laravel's plural table spells locales en_US, so a hyphenated BCP 47 tag gets the default rule, which is always index 0, so getPluralIndex("en-US", 5) is 0. This port matches Laravel and adds normalizeLocale to convert a tag, which you apply to the locale and to the keys of your lines alike.
import { getPluralIndex, normalizeLocale } from "@tolki/lang";
getPluralIndex("pt-BR", 2);
// => 0
getPluralIndex(normalizeLocale("pt-BR"), 2);
// => 1
getPluralIndex(normalizeLocale("pt-BR"), 1);
// => 0A new Translator has no fallback locale, as in Laravel, where only the service provider sets one. Call setFallback(). See the warning on setting a fallback.
Tag keys match literally
Laravel builds the pattern for a tag callback from the key as it is, so a key a.b also matches <aXb>. This port escapes the key, and a.b matches only <a.b>.
import { makeReplacements } from "@tolki/lang";
makeReplacements("<a.b>hi</a.b> <aXb>hi</aXb>", {
"a.b": (text) => text.toUpperCase(),
});
// => "HI <aXb>hi</aXb>"Replacements are not escaped
Tag callbacks and placeholder values are inserted unescaped, as in PHP, so never pass a line built from untrusted values to v-html or dangerouslySetInnerHTML.
import { makeReplacements } from "@tolki/lang";
makeReplacements("Hi :name", { name: "<b>Ann</b>" });
// => "Hi <b>Ann</b>"
makeReplacements("<b>hi</b>", { b: (text) => `<i>${text}</i>` });
// => "<i>hi</i>"Objects need a handler
PHP can print an object that has __toString, an enum case, and a Carbon date. In JavaScript only an object with its own toString and an @tolki/enum case can. A Date, or any object without its own toString, throws TypeError, as PHP throws for a DateTime, unless stringable(Class, handler) covers its class. A handler runs for exact instances of the class, so register a subclass too.
import { Translator } from "@tolki/lang";
const translator = new Translator({ en: {} }, "en");
translator.get("Due :date", { date: new Date(0) });
// => throws TypeError
translator.stringable(Date, (date) => date.toISOString().slice(0, 10));
translator.get("Due :date", { date: new Date(0) });
// => "Due 1970-01-01"Laravel's one-argument form, stringable(fn (Money $money) => …), reads the class from the parameter's type. JavaScript has no parameter types to read, so only the two-argument form exists.
Title case
For :Name, PHP title-cases the first character with mb_ucfirst. This port uses ucfirst from @tolki/str, which upper-cases it. These two characters are known to differ: ß, which PHP title-cases to Ss and this port to SS, and dž, which PHP title-cases to Dž and this port to DŽ.
import { makeReplacements } from "@tolki/lang";
makeReplacements(":Name", { name: "ßtraße" });
// => "SStraße"
makeReplacements(":Name", { name: "džemal" });
// => "DŽemal"No file loading
The caller passes the lines. There is no loader, so the translator never reads lang/en.json or lang/en/messages.php, and there is no addNamespace, addPath or addJsonPath. Build the lines object on the server, as shown in Installation & Usage, or import your JSON files.
Not ported
load,isLoaded,getLoader,addNamespace,addPath,addJsonPath,FileLoader,ArrayLoaderandTranslationServiceProvider: loading lang files is the caller's job here.getSelectorandsetSelector: the selector is the stateless functionschooseandgetPluralIndex.NamespacedItemResolver::setParsedKeyandflushParsedKeys, and the parse cache: the cache is invisible except through those two, which exist for the config repository.Macroable, andstringable()with a single closure argument: JavaScript has neither macros on this class nor parameter types to read.- The
nameof a pure (unit) enum: an@tolki/enumcase always has avalue. PotentiallyTranslatedString,CreatesPotentiallyTranslatedStringsandHasLocalePreference: they are plumbing for validation and notifications.