@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
|
@@ -1434,10 +1434,10 @@ const setRuntimeLangSource = (source) => {
|
|
|
1434
1434
|
* ```
|
|
1435
1435
|
*
|
|
1436
1436
|
* @param {string} [options.fallbackLang]
|
|
1437
|
-
* Language consulted
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1437
|
+
* Language consulted for a key none of the requested languages translates,
|
|
1438
|
+
* before giving up and returning the key itself. Set it to a language holding
|
|
1439
|
+
* every key whenever keys are opaque: without it, a reader whose languages
|
|
1440
|
+
* all miss a key sees that key's raw name.
|
|
1441
1441
|
*
|
|
1442
1442
|
* @param {string|string[]} [options.runtimeLang]
|
|
1443
1443
|
* The active language (BCP 47 tag or ordered array of tags) — named
|
|
@@ -1480,19 +1480,27 @@ const setRuntimeLangSource = (source) => {
|
|
|
1480
1480
|
*
|
|
1481
1481
|
* **`i18n(key, values?, { lang? })`** — the translation for `key`, with
|
|
1482
1482
|
* `[placeholder]` occurrences replaced from `values` (see `interpolateText`).
|
|
1483
|
-
*
|
|
1484
|
-
* renders something readable. `i18n.format` is an alias of this call.
|
|
1483
|
+
* `i18n.format` is an alias of this call.
|
|
1485
1484
|
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1485
|
+
* The language is resolved per key, not once for the whole registry: each
|
|
1486
|
+
* requested language in order (`lang`, else `runtimeLang` — a regional tag
|
|
1487
|
+
* reaching its registered parent, `"de-DE"` → `"de"`), then `fallbackLang`;
|
|
1488
|
+
* the first one translating `key` wins. A language translated for only part of
|
|
1489
|
+
* the keys therefore reads its own words where it has them and the next
|
|
1490
|
+
* language's everywhere else. When none translates it, `key` itself comes
|
|
1491
|
+
* back, so an untranslated string still renders something readable.
|
|
1492
|
+
*
|
|
1493
|
+
* **`i18n.has(key, { lang? })`** — whether a translation genuinely exists in
|
|
1494
|
+
* one of the languages above, i.e. how to tell "no translation" apart from
|
|
1495
|
+
* "translation equal to the key".
|
|
1488
1496
|
*
|
|
1489
1497
|
* @returns {Function & { add, addAll, addLangKeys, has, format, languageMap }}
|
|
1490
1498
|
*/
|
|
1491
1499
|
const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
|
|
1492
1500
|
const languageMap = new Map();
|
|
1493
|
-
// Bumped by addLangKeys — the only thing besides the
|
|
1494
|
-
// that could change what
|
|
1495
|
-
//
|
|
1501
|
+
// Bumped by addLangKeys — the only thing besides the requested lang itself
|
|
1502
|
+
// that could change what resolveLangChain() below resolves to, so it's what
|
|
1503
|
+
// invalidates its small cache.
|
|
1496
1504
|
let languageMapVersion = 0;
|
|
1497
1505
|
|
|
1498
1506
|
// Without an explicit runtimeLang, the runtime language source is re-read
|
|
@@ -1500,49 +1508,39 @@ const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
|
|
|
1500
1508
|
// ignore an app-wide language change (see runtime_lang.js) for the rest of
|
|
1501
1509
|
// this instance's life.
|
|
1502
1510
|
const hasExplicitRuntimeLang = runtimeLang !== undefined;
|
|
1511
|
+
const getDefaultLang = () => {
|
|
1512
|
+
return hasExplicitRuntimeLang ? runtimeLang : getRuntimeLang();
|
|
1513
|
+
};
|
|
1503
1514
|
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
let cachedActiveLang;
|
|
1514
|
-
let cachedActiveLangRuntimeLang;
|
|
1515
|
-
let cachedActiveLangVersion = -1;
|
|
1516
|
-
const getActiveLang = () => {
|
|
1517
|
-
const currentRuntimeLang = hasExplicitRuntimeLang
|
|
1518
|
-
? runtimeLang
|
|
1519
|
-
: getRuntimeLang();
|
|
1515
|
+
// Walked per key rather than one language picked for the whole registry: a
|
|
1516
|
+
// registry is rarely translated evenly (@jsenv/humanize ships German time
|
|
1517
|
+
// words, navi ships its buttons in en/fr only), and a single language would
|
|
1518
|
+
// show the raw name of every key it lacks. Cached on the lang reference,
|
|
1519
|
+
// which the runtime lang source keeps stable while nothing changed.
|
|
1520
|
+
let cachedLangChain;
|
|
1521
|
+
let cachedLangChainLang;
|
|
1522
|
+
let cachedLangChainVersion = -1;
|
|
1523
|
+
const resolveLangChain = (lang) => {
|
|
1520
1524
|
if (
|
|
1521
|
-
|
|
1522
|
-
|
|
1525
|
+
cachedLangChainVersion === languageMapVersion &&
|
|
1526
|
+
cachedLangChainLang === lang
|
|
1523
1527
|
) {
|
|
1524
|
-
return
|
|
1525
|
-
}
|
|
1526
|
-
cachedActiveLang = matchBestLang(currentRuntimeLang, languageMap);
|
|
1527
|
-
cachedActiveLangVersion = languageMapVersion;
|
|
1528
|
-
cachedActiveLangRuntimeLang = currentRuntimeLang;
|
|
1529
|
-
return cachedActiveLang;
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
// fallbackLang is a plain, never-reactive option set once at creation —
|
|
1533
|
-
// its own resolution only ever needs recomputing when languageMap does.
|
|
1534
|
-
let cachedResolvedFallbackLang;
|
|
1535
|
-
let cachedResolvedFallbackLangVersion = -1;
|
|
1536
|
-
const getResolvedFallbackLang = () => {
|
|
1537
|
-
if (!fallbackLang) {
|
|
1538
|
-
return null;
|
|
1528
|
+
return cachedLangChain;
|
|
1539
1529
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1530
|
+
const langChain = [];
|
|
1531
|
+
for (const candidate of [
|
|
1532
|
+
...toLangList(lang),
|
|
1533
|
+
...toLangList(fallbackLang),
|
|
1534
|
+
]) {
|
|
1535
|
+
const match = matchLang(candidate, languageMap);
|
|
1536
|
+
if (match && !langChain.includes(match)) {
|
|
1537
|
+
langChain.push(match);
|
|
1538
|
+
}
|
|
1542
1539
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1540
|
+
cachedLangChain = langChain;
|
|
1541
|
+
cachedLangChainLang = lang;
|
|
1542
|
+
cachedLangChainVersion = languageMapVersion;
|
|
1543
|
+
return langChain;
|
|
1546
1544
|
};
|
|
1547
1545
|
|
|
1548
1546
|
const addLangKeys = (lang, translations) => {
|
|
@@ -1581,47 +1579,25 @@ const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
|
|
|
1581
1579
|
}
|
|
1582
1580
|
};
|
|
1583
1581
|
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
// .split() otherwise.
|
|
1588
|
-
const resolvedLang = lang ? matchBestLang(lang, languageMap) : null;
|
|
1589
|
-
if (resolvedLang) {
|
|
1590
|
-
const translations = languageMap.get(resolvedLang);
|
|
1591
|
-
const translated = translations[key];
|
|
1582
|
+
const getTemplate = (key, lang) => {
|
|
1583
|
+
for (const resolvedLang of resolveLangChain(lang)) {
|
|
1584
|
+
const translated = languageMap.get(resolvedLang)[key];
|
|
1592
1585
|
if (translated !== undefined) {
|
|
1593
1586
|
return translated;
|
|
1594
1587
|
}
|
|
1595
1588
|
}
|
|
1596
|
-
const resolvedFallbackLang = getResolvedFallbackLang();
|
|
1597
|
-
if (resolvedFallbackLang) {
|
|
1598
|
-
const fallbackTranslations = languageMap.get(resolvedFallbackLang);
|
|
1599
|
-
const fallbackTranslated = fallbackTranslations[key];
|
|
1600
|
-
if (fallbackTranslated !== undefined) {
|
|
1601
|
-
return fallbackTranslated;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
1589
|
// No translation found — return key as-is (opaque fallback)
|
|
1605
1590
|
return key;
|
|
1606
1591
|
};
|
|
1607
1592
|
|
|
1608
|
-
const format = (key, values, { lang =
|
|
1609
|
-
const template =
|
|
1593
|
+
const format = (key, values, { lang = getDefaultLang() } = {}) => {
|
|
1594
|
+
const template = getTemplate(key, lang);
|
|
1610
1595
|
return interpolateText(template, values);
|
|
1611
1596
|
};
|
|
1612
1597
|
|
|
1613
|
-
const has = (key, { lang =
|
|
1614
|
-
const resolvedLang
|
|
1615
|
-
|
|
1616
|
-
const translations = languageMap.get(resolvedLang);
|
|
1617
|
-
if (translations && key in translations) {
|
|
1618
|
-
return true;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
const resolvedFallbackLang = getResolvedFallbackLang();
|
|
1622
|
-
if (resolvedFallbackLang) {
|
|
1623
|
-
const fallbackTranslations = languageMap.get(resolvedFallbackLang);
|
|
1624
|
-
if (fallbackTranslations && key in fallbackTranslations) {
|
|
1598
|
+
const has = (key, { lang = getDefaultLang() } = {}) => {
|
|
1599
|
+
for (const resolvedLang of resolveLangChain(lang)) {
|
|
1600
|
+
if (key in languageMap.get(resolvedLang)) {
|
|
1625
1601
|
return true;
|
|
1626
1602
|
}
|
|
1627
1603
|
}
|
|
@@ -1656,19 +1632,15 @@ const matchLang = (lang, languageMap) => {
|
|
|
1656
1632
|
return null;
|
|
1657
1633
|
};
|
|
1658
1634
|
|
|
1659
|
-
// lang can be a string
|
|
1660
|
-
const
|
|
1635
|
+
// lang can be a string, an ordered array of preference strings, or nothing
|
|
1636
|
+
const toLangList = (lang) => {
|
|
1661
1637
|
if (!lang) {
|
|
1662
|
-
return
|
|
1638
|
+
return [];
|
|
1663
1639
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const match = matchLang(candidate, languageMap);
|
|
1667
|
-
if (match) {
|
|
1668
|
-
return match;
|
|
1669
|
-
}
|
|
1640
|
+
if (Array.isArray(lang)) {
|
|
1641
|
+
return lang;
|
|
1670
1642
|
}
|
|
1671
|
-
return
|
|
1643
|
+
return [lang];
|
|
1672
1644
|
};
|
|
1673
1645
|
|
|
1674
1646
|
/**
|
|
@@ -1688,6 +1660,11 @@ const matchBestLang = (lang, languageMap) => {
|
|
|
1688
1660
|
* — the opposite of what an app is advised to do for its own texts; navi's
|
|
1689
1661
|
* `docs/i18n.md` explains why.
|
|
1690
1662
|
*
|
|
1663
|
+
* English translates every key, so it is the `fallbackLang`: the other
|
|
1664
|
+
* languages cover part of the keys only (German has the `time.*` words, not
|
|
1665
|
+
* navi's buttons), and a key the reader's languages all lack reads in English
|
|
1666
|
+
* rather than as its raw name.
|
|
1667
|
+
*
|
|
1691
1668
|
* @example
|
|
1692
1669
|
* import { humanizeI18n } from "@jsenv/humanize";
|
|
1693
1670
|
*
|
|
@@ -1697,7 +1674,7 @@ const matchBestLang = (lang, languageMap) => {
|
|
|
1697
1674
|
* // Teach a language that is not shipped:
|
|
1698
1675
|
* humanizeI18n.addLangKeys("ja", { "time.midnight": "真夜中" });
|
|
1699
1676
|
*/
|
|
1700
|
-
const humanizeI18n = createI18n();
|
|
1677
|
+
const humanizeI18n = createI18n({ fallbackLang: "en" });
|
|
1701
1678
|
|
|
1702
1679
|
// What the time formatters in ../time/format_time.js write in words:
|
|
1703
1680
|
// relative wording, the midnight word, the mark between the two bounds of a
|
|
@@ -4157,7 +4134,8 @@ const startSpinner = ({
|
|
|
4157
4134
|
|
|
4158
4135
|
const createTaskLog = (
|
|
4159
4136
|
label,
|
|
4160
|
-
|
|
4137
|
+
// animated defaults to what the spinner decides: only when stdout is a terminal
|
|
4138
|
+
{ disabled = false, animated, stopOnWriteFromOutside } = {},
|
|
4161
4139
|
) => {
|
|
4162
4140
|
if (disabled) {
|
|
4163
4141
|
return {
|
|
@@ -4167,7 +4145,7 @@ const createTaskLog = (
|
|
|
4167
4145
|
fail: () => {},
|
|
4168
4146
|
};
|
|
4169
4147
|
}
|
|
4170
|
-
if (
|
|
4148
|
+
if (process.env.CAPTURING_SIDE_EFFECTS) {
|
|
4171
4149
|
animated = false;
|
|
4172
4150
|
}
|
|
4173
4151
|
const startMs = Date.now();
|