@jsenv/humanize 1.8.4 → 1.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/jsenv_humanize_browser.js +67 -90
- package/dist/browser/jsenv_humanize_browser.js.map +3 -3
- package/dist/node/jsenv_humanize_node.js +70 -92
- package/dist/node/jsenv_humanize_node.js.map +4 -4
- package/dist/node/jsenv_humanize_node_modules.js +20 -10
- package/dist/node/jsenv_humanize_node_modules.js.map +6 -6
- package/package.json +2 -2
|
@@ -1418,10 +1418,10 @@ const setRuntimeLangSource = (source) => {
|
|
|
1418
1418
|
* ```
|
|
1419
1419
|
*
|
|
1420
1420
|
* @param {string} [options.fallbackLang]
|
|
1421
|
-
* Language consulted
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1421
|
+
* Language consulted for a key none of the requested languages translates,
|
|
1422
|
+
* before giving up and returning the key itself. Set it to a language holding
|
|
1423
|
+
* every key whenever keys are opaque: without it, a reader whose languages
|
|
1424
|
+
* all miss a key sees that key's raw name.
|
|
1425
1425
|
*
|
|
1426
1426
|
* @param {string|string[]} [options.runtimeLang]
|
|
1427
1427
|
* The active language (BCP 47 tag or ordered array of tags) — named
|
|
@@ -1464,19 +1464,27 @@ const setRuntimeLangSource = (source) => {
|
|
|
1464
1464
|
*
|
|
1465
1465
|
* **`i18n(key, values?, { lang? })`** — the translation for `key`, with
|
|
1466
1466
|
* `[placeholder]` occurrences replaced from `values` (see `interpolateText`).
|
|
1467
|
-
*
|
|
1468
|
-
* renders something readable. `i18n.format` is an alias of this call.
|
|
1467
|
+
* `i18n.format` is an alias of this call.
|
|
1469
1468
|
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1469
|
+
* The language is resolved per key, not once for the whole registry: each
|
|
1470
|
+
* requested language in order (`lang`, else `runtimeLang` — a regional tag
|
|
1471
|
+
* reaching its registered parent, `"de-DE"` → `"de"`), then `fallbackLang`;
|
|
1472
|
+
* the first one translating `key` wins. A language translated for only part of
|
|
1473
|
+
* the keys therefore reads its own words where it has them and the next
|
|
1474
|
+
* language's everywhere else. When none translates it, `key` itself comes
|
|
1475
|
+
* back, so an untranslated string still renders something readable.
|
|
1476
|
+
*
|
|
1477
|
+
* **`i18n.has(key, { lang? })`** — whether a translation genuinely exists in
|
|
1478
|
+
* one of the languages above, i.e. how to tell "no translation" apart from
|
|
1479
|
+
* "translation equal to the key".
|
|
1472
1480
|
*
|
|
1473
1481
|
* @returns {Function & { add, addAll, addLangKeys, has, format, languageMap }}
|
|
1474
1482
|
*/
|
|
1475
1483
|
const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
|
|
1476
1484
|
const languageMap = new Map();
|
|
1477
|
-
// Bumped by addLangKeys — the only thing besides the
|
|
1478
|
-
// that could change what
|
|
1479
|
-
//
|
|
1485
|
+
// Bumped by addLangKeys — the only thing besides the requested lang itself
|
|
1486
|
+
// that could change what resolveLangChain() below resolves to, so it's what
|
|
1487
|
+
// invalidates its small cache.
|
|
1480
1488
|
let languageMapVersion = 0;
|
|
1481
1489
|
|
|
1482
1490
|
// Without an explicit runtimeLang, the runtime language source is re-read
|
|
@@ -1484,49 +1492,39 @@ const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
|
|
|
1484
1492
|
// ignore an app-wide language change (see runtime_lang.js) for the rest of
|
|
1485
1493
|
// this instance's life.
|
|
1486
1494
|
const hasExplicitRuntimeLang = runtimeLang !== undefined;
|
|
1495
|
+
const getDefaultLang = () => {
|
|
1496
|
+
return hasExplicitRuntimeLang ? runtimeLang : getRuntimeLang();
|
|
1497
|
+
};
|
|
1487
1498
|
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
let cachedActiveLang;
|
|
1498
|
-
let cachedActiveLangRuntimeLang;
|
|
1499
|
-
let cachedActiveLangVersion = -1;
|
|
1500
|
-
const getActiveLang = () => {
|
|
1501
|
-
const currentRuntimeLang = hasExplicitRuntimeLang
|
|
1502
|
-
? runtimeLang
|
|
1503
|
-
: getRuntimeLang();
|
|
1499
|
+
// Walked per key rather than one language picked for the whole registry: a
|
|
1500
|
+
// registry is rarely translated evenly (@jsenv/humanize ships German time
|
|
1501
|
+
// words, navi ships its buttons in en/fr only), and a single language would
|
|
1502
|
+
// show the raw name of every key it lacks. Cached on the lang reference,
|
|
1503
|
+
// which the runtime lang source keeps stable while nothing changed.
|
|
1504
|
+
let cachedLangChain;
|
|
1505
|
+
let cachedLangChainLang;
|
|
1506
|
+
let cachedLangChainVersion = -1;
|
|
1507
|
+
const resolveLangChain = (lang) => {
|
|
1504
1508
|
if (
|
|
1505
|
-
|
|
1506
|
-
|
|
1509
|
+
cachedLangChainVersion === languageMapVersion &&
|
|
1510
|
+
cachedLangChainLang === lang
|
|
1507
1511
|
) {
|
|
1508
|
-
return
|
|
1509
|
-
}
|
|
1510
|
-
cachedActiveLang = matchBestLang(currentRuntimeLang, languageMap);
|
|
1511
|
-
cachedActiveLangVersion = languageMapVersion;
|
|
1512
|
-
cachedActiveLangRuntimeLang = currentRuntimeLang;
|
|
1513
|
-
return cachedActiveLang;
|
|
1514
|
-
};
|
|
1515
|
-
|
|
1516
|
-
// fallbackLang is a plain, never-reactive option set once at creation —
|
|
1517
|
-
// its own resolution only ever needs recomputing when languageMap does.
|
|
1518
|
-
let cachedResolvedFallbackLang;
|
|
1519
|
-
let cachedResolvedFallbackLangVersion = -1;
|
|
1520
|
-
const getResolvedFallbackLang = () => {
|
|
1521
|
-
if (!fallbackLang) {
|
|
1522
|
-
return null;
|
|
1512
|
+
return cachedLangChain;
|
|
1523
1513
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1514
|
+
const langChain = [];
|
|
1515
|
+
for (const candidate of [
|
|
1516
|
+
...toLangList(lang),
|
|
1517
|
+
...toLangList(fallbackLang),
|
|
1518
|
+
]) {
|
|
1519
|
+
const match = matchLang(candidate, languageMap);
|
|
1520
|
+
if (match && !langChain.includes(match)) {
|
|
1521
|
+
langChain.push(match);
|
|
1522
|
+
}
|
|
1526
1523
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1524
|
+
cachedLangChain = langChain;
|
|
1525
|
+
cachedLangChainLang = lang;
|
|
1526
|
+
cachedLangChainVersion = languageMapVersion;
|
|
1527
|
+
return langChain;
|
|
1530
1528
|
};
|
|
1531
1529
|
|
|
1532
1530
|
const addLangKeys = (lang, translations) => {
|
|
@@ -1565,47 +1563,25 @@ const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
|
|
|
1565
1563
|
}
|
|
1566
1564
|
};
|
|
1567
1565
|
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
// .split() otherwise.
|
|
1572
|
-
const resolvedLang = lang ? matchBestLang(lang, languageMap) : null;
|
|
1573
|
-
if (resolvedLang) {
|
|
1574
|
-
const translations = languageMap.get(resolvedLang);
|
|
1575
|
-
const translated = translations[key];
|
|
1566
|
+
const getTemplate = (key, lang) => {
|
|
1567
|
+
for (const resolvedLang of resolveLangChain(lang)) {
|
|
1568
|
+
const translated = languageMap.get(resolvedLang)[key];
|
|
1576
1569
|
if (translated !== undefined) {
|
|
1577
1570
|
return translated;
|
|
1578
1571
|
}
|
|
1579
1572
|
}
|
|
1580
|
-
const resolvedFallbackLang = getResolvedFallbackLang();
|
|
1581
|
-
if (resolvedFallbackLang) {
|
|
1582
|
-
const fallbackTranslations = languageMap.get(resolvedFallbackLang);
|
|
1583
|
-
const fallbackTranslated = fallbackTranslations[key];
|
|
1584
|
-
if (fallbackTranslated !== undefined) {
|
|
1585
|
-
return fallbackTranslated;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
1573
|
// No translation found — return key as-is (opaque fallback)
|
|
1589
1574
|
return key;
|
|
1590
1575
|
};
|
|
1591
1576
|
|
|
1592
|
-
const format = (key, values, { lang =
|
|
1593
|
-
const template =
|
|
1577
|
+
const format = (key, values, { lang = getDefaultLang() } = {}) => {
|
|
1578
|
+
const template = getTemplate(key, lang);
|
|
1594
1579
|
return interpolateText(template, values);
|
|
1595
1580
|
};
|
|
1596
1581
|
|
|
1597
|
-
const has = (key, { lang =
|
|
1598
|
-
const resolvedLang
|
|
1599
|
-
|
|
1600
|
-
const translations = languageMap.get(resolvedLang);
|
|
1601
|
-
if (translations && key in translations) {
|
|
1602
|
-
return true;
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
const resolvedFallbackLang = getResolvedFallbackLang();
|
|
1606
|
-
if (resolvedFallbackLang) {
|
|
1607
|
-
const fallbackTranslations = languageMap.get(resolvedFallbackLang);
|
|
1608
|
-
if (fallbackTranslations && key in fallbackTranslations) {
|
|
1582
|
+
const has = (key, { lang = getDefaultLang() } = {}) => {
|
|
1583
|
+
for (const resolvedLang of resolveLangChain(lang)) {
|
|
1584
|
+
if (key in languageMap.get(resolvedLang)) {
|
|
1609
1585
|
return true;
|
|
1610
1586
|
}
|
|
1611
1587
|
}
|
|
@@ -1640,19 +1616,15 @@ const matchLang = (lang, languageMap) => {
|
|
|
1640
1616
|
return null;
|
|
1641
1617
|
};
|
|
1642
1618
|
|
|
1643
|
-
// lang can be a string
|
|
1644
|
-
const
|
|
1619
|
+
// lang can be a string, an ordered array of preference strings, or nothing
|
|
1620
|
+
const toLangList = (lang) => {
|
|
1645
1621
|
if (!lang) {
|
|
1646
|
-
return
|
|
1622
|
+
return [];
|
|
1647
1623
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
const match = matchLang(candidate, languageMap);
|
|
1651
|
-
if (match) {
|
|
1652
|
-
return match;
|
|
1653
|
-
}
|
|
1624
|
+
if (Array.isArray(lang)) {
|
|
1625
|
+
return lang;
|
|
1654
1626
|
}
|
|
1655
|
-
return
|
|
1627
|
+
return [lang];
|
|
1656
1628
|
};
|
|
1657
1629
|
|
|
1658
1630
|
/**
|
|
@@ -1672,6 +1644,11 @@ const matchBestLang = (lang, languageMap) => {
|
|
|
1672
1644
|
* — the opposite of what an app is advised to do for its own texts; navi's
|
|
1673
1645
|
* `docs/i18n.md` explains why.
|
|
1674
1646
|
*
|
|
1647
|
+
* English translates every key, so it is the `fallbackLang`: the other
|
|
1648
|
+
* languages cover part of the keys only (German has the `time.*` words, not
|
|
1649
|
+
* navi's buttons), and a key the reader's languages all lack reads in English
|
|
1650
|
+
* rather than as its raw name.
|
|
1651
|
+
*
|
|
1675
1652
|
* @example
|
|
1676
1653
|
* import { humanizeI18n } from "@jsenv/humanize";
|
|
1677
1654
|
*
|
|
@@ -1681,7 +1658,7 @@ const matchBestLang = (lang, languageMap) => {
|
|
|
1681
1658
|
* // Teach a language that is not shipped:
|
|
1682
1659
|
* humanizeI18n.addLangKeys("ja", { "time.midnight": "真夜中" });
|
|
1683
1660
|
*/
|
|
1684
|
-
const humanizeI18n = createI18n();
|
|
1661
|
+
const humanizeI18n = createI18n({ fallbackLang: "en" });
|
|
1685
1662
|
|
|
1686
1663
|
// What the time formatters in ../time/format_time.js write in words:
|
|
1687
1664
|
// relative wording, the midnight word, the mark between the two bounds of a
|