@fedify/vocab-runtime 2.0.31-dev.2306 → 2.0.31-dev.2308

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.0.31-dev.2306+e775cf8c",
3
+ "version": "2.0.31-dev.2308+34090b3a",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -4504,7 +4504,7 @@ const preloadedContexts = {
4504
4504
  //#endregion
4505
4505
  //#region deno.json
4506
4506
  var name = "@fedify/vocab-runtime";
4507
- var version = "2.0.31-dev.2306+e775cf8c";
4507
+ var version = "2.0.31-dev.2308+34090b3a";
4508
4508
  //#endregion
4509
4509
  //#region src/request.ts
4510
4510
  /**
@@ -5730,6 +5730,50 @@ async function exportMultibaseKey(key) {
5730
5730
  return new TextDecoder().decode(encoded);
5731
5731
  }
5732
5732
  //#endregion
5733
+ //#region src/langtag.ts
5734
+ const EXTLANG_PAIRS = new Set(Object.entries({
5735
+ ar: "aao abh abv acm acq acw acx acy adf aeb aec afb ajp apc apd arb arq ars ary arz auz avl ayh ayl ayn ayp bbz pga shu ssh",
5736
+ kok: "gom knn",
5737
+ lv: "ltg lvs",
5738
+ ms: "bjn btj bve bvu coa dup hji jak jax kvb kvr kxd lce lcf liw max meo mfa mfb min mqg msi mui orn ors pel pse tmw urk vkk vkt xmm zlm zmi zsm",
5739
+ sgn: "ads aed aen afg ajs ase asf asp asq asw bfi bfk bog bqn bqy bvl bzs cds csc csd cse csf csg csl csn csq csr csx doq dse dsl dsz dyl ecs ehs esl esn eso eth fcs fse fsl fss gds gse gsg gsm gss gus hab haf hds hks hos hps hsh hsl icl iks ils inl ins ise isg isr jcs jhs jks jls jos jsl jus kgi kvk lbs lgs lls lsb lsc lsg lsl lsn lso lsp lst lsv lsw lsy lws mdl mfs mre msd msr mzc mzg mzy nbs ncs nsi nsl nsp nsr nzs okl pgz pks prl prz psc psd psg psl pso psp psr pys rib rms rnb rsi rsl rsm rsn sdl sfb sfs sgg sgx slf sls sqk sqs sqx ssp ssr svk swl syy szs tse tsm tsq tss tsy tza ugn ugy ukl uks vgt vsi vsl vsv wbs xki xml xms yds ygs yhs ysl ysm zhk zib zsl",
5740
+ sw: "swc swh",
5741
+ uz: "uzn uzs",
5742
+ zh: "cdo cjy cmn cnp cpx csp czh czo gan hak hnm hsn luh lzh mnp nan sjc wuu yue"
5743
+ }).flatMap(([prefix, extlangs]) => extlangs.split(" ").map((extlang) => `${prefix}-${extlang}`)));
5744
+ const PREFIX_PATTERN = /^[A-Za-z]{2,3}$/;
5745
+ const EXTLANG_PATTERN = /^[A-Za-z]{3}$/;
5746
+ /**
5747
+ * Normalizes a BCP 47 language tag that has an extended language subtag
5748
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
5749
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
5750
+ * tags, so tags should be normalized before being passed to it.
5751
+ *
5752
+ * Only registered extlangs that follow their registered prefix are removed;
5753
+ * any other tag is returned unchanged, so the result still needs to be
5754
+ * validated, e.g., by `Intl.Locale`.
5755
+ *
5756
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
5757
+ * releases. When merging it into the *main* branch, remove the `@internal`
5758
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
5759
+ * the changelog fragment.
5760
+ *
5761
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
5762
+ *
5763
+ * @param tag The BCP 47 language tag to normalize.
5764
+ * @returns The normalized language tag.
5765
+ * @internal
5766
+ */
5767
+ function normalizeLanguageTag(tag) {
5768
+ const subtags = tag.split("-");
5769
+ if (subtags.length < 2) return tag;
5770
+ const [prefix, extlang, ...rest] = subtags;
5771
+ if (!PREFIX_PATTERN.test(prefix) || !EXTLANG_PATTERN.test(extlang)) return tag;
5772
+ const language = extlang.toLowerCase();
5773
+ if (!EXTLANG_PAIRS.has(`${prefix.toLowerCase()}-${language}`)) return tag;
5774
+ return [language, ...rest].join("-");
5775
+ }
5776
+ //#endregion
5733
5777
  //#region src/langstr.ts
5734
5778
  /**
5735
5779
  * A language-tagged string which corresponds to the `rdf:langString` type.
@@ -5744,11 +5788,13 @@ var LanguageString = class extends String {
5744
5788
  * Constructs a new `LanguageString`.
5745
5789
  * @param value A string value written in the given language.
5746
5790
  * @param language The language of the string. If a string is given, it will
5747
- * be parsed as a `Intl.Locale` object.
5791
+ * be parsed as a `Intl.Locale` object. Extended language
5792
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
5793
+ * `yue`.
5748
5794
  */
5749
5795
  constructor(value, language) {
5750
5796
  super(value);
5751
- this.locale = typeof language === "string" ? new Intl.Locale(language) : language;
5797
+ this.locale = typeof language === "string" ? new Intl.Locale(normalizeLanguageTag(language)) : language;
5752
5798
  }
5753
5799
  };
5754
5800
  LanguageString.prototype[Symbol.for("Deno.customInspect")] = function(inspect, options) {
@@ -5778,5 +5824,6 @@ exports.importSpki = importSpki;
5778
5824
  exports.isValidPublicIPv4Address = isValidPublicIPv4Address;
5779
5825
  exports.isValidPublicIPv6Address = isValidPublicIPv6Address;
5780
5826
  exports.logRequest = logRequest;
5827
+ exports.normalizeLanguageTag = normalizeLanguageTag;
5781
5828
  exports.preloadedContexts = preloadedContexts;
5782
5829
  exports.validatePublicUrl = validatePublicUrl;
package/dist/mod.d.cts CHANGED
@@ -265,11 +265,37 @@ declare class LanguageString extends String {
265
265
  * Constructs a new `LanguageString`.
266
266
  * @param value A string value written in the given language.
267
267
  * @param language The language of the string. If a string is given, it will
268
- * be parsed as a `Intl.Locale` object.
268
+ * be parsed as a `Intl.Locale` object. Extended language
269
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
270
+ * `yue`.
269
271
  */
270
272
  constructor(value: string, language: Intl.Locale | string);
271
273
  }
272
274
  //#endregion
275
+ //#region src/langtag.d.ts
276
+ /**
277
+ * Normalizes a BCP 47 language tag that has an extended language subtag
278
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
279
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
280
+ * tags, so tags should be normalized before being passed to it.
281
+ *
282
+ * Only registered extlangs that follow their registered prefix are removed;
283
+ * any other tag is returned unchanged, so the result still needs to be
284
+ * validated, e.g., by `Intl.Locale`.
285
+ *
286
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
287
+ * releases. When merging it into the *main* branch, remove the `@internal`
288
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
289
+ * the changelog fragment.
290
+ *
291
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
292
+ *
293
+ * @param tag The BCP 47 language tag to normalize.
294
+ * @returns The normalized language tag.
295
+ * @internal
296
+ */
297
+ declare function normalizeLanguageTag(tag: string): string;
298
+ //#endregion
273
299
  //#region src/multibase/types.d.ts
274
300
  type BaseCode = "\x00" | "0" | "7" | "9" | "f" | "F" | "v" | "V" | "t" | "T" | "b" | "B" | "c" | "C" | "h" | "k" | "K" | "z" | "Z" | "m" | "M" | "u" | "U";
275
301
  /**
@@ -344,4 +370,4 @@ declare function isValidPublicIPv4Address(address: string): boolean;
344
370
  declare function isValidPublicIPv6Address(address: string): boolean;
345
371
  declare function expandIPv6Address(address: string): string;
346
372
  //#endregion
347
- export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, LanguageString, type RemoteDocument, UrlError, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, preloadedContexts, validatePublicUrl };
373
+ export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, LanguageString, type RemoteDocument, UrlError, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, normalizeLanguageTag, preloadedContexts, validatePublicUrl };
package/dist/mod.d.ts CHANGED
@@ -264,11 +264,37 @@ declare class LanguageString extends String {
264
264
  * Constructs a new `LanguageString`.
265
265
  * @param value A string value written in the given language.
266
266
  * @param language The language of the string. If a string is given, it will
267
- * be parsed as a `Intl.Locale` object.
267
+ * be parsed as a `Intl.Locale` object. Extended language
268
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
269
+ * `yue`.
268
270
  */
269
271
  constructor(value: string, language: Intl.Locale | string);
270
272
  }
271
273
  //#endregion
274
+ //#region src/langtag.d.ts
275
+ /**
276
+ * Normalizes a BCP 47 language tag that has an extended language subtag
277
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
278
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
279
+ * tags, so tags should be normalized before being passed to it.
280
+ *
281
+ * Only registered extlangs that follow their registered prefix are removed;
282
+ * any other tag is returned unchanged, so the result still needs to be
283
+ * validated, e.g., by `Intl.Locale`.
284
+ *
285
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
286
+ * releases. When merging it into the *main* branch, remove the `@internal`
287
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
288
+ * the changelog fragment.
289
+ *
290
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
291
+ *
292
+ * @param tag The BCP 47 language tag to normalize.
293
+ * @returns The normalized language tag.
294
+ * @internal
295
+ */
296
+ declare function normalizeLanguageTag(tag: string): string;
297
+ //#endregion
272
298
  //#region src/multibase/types.d.ts
273
299
  type BaseCode = "\x00" | "0" | "7" | "9" | "f" | "F" | "v" | "V" | "t" | "T" | "b" | "B" | "c" | "C" | "h" | "k" | "K" | "z" | "Z" | "m" | "M" | "u" | "U";
274
300
  /**
@@ -343,4 +369,4 @@ declare function isValidPublicIPv4Address(address: string): boolean;
343
369
  declare function isValidPublicIPv6Address(address: string): boolean;
344
370
  declare function expandIPv6Address(address: string): string;
345
371
  //#endregion
346
- export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, LanguageString, type RemoteDocument, UrlError, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, preloadedContexts, validatePublicUrl };
372
+ export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, LanguageString, type RemoteDocument, UrlError, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, normalizeLanguageTag, preloadedContexts, validatePublicUrl };
package/dist/mod.js CHANGED
@@ -4499,7 +4499,7 @@ const preloadedContexts = {
4499
4499
  //#endregion
4500
4500
  //#region deno.json
4501
4501
  var name = "@fedify/vocab-runtime";
4502
- var version = "2.0.31-dev.2306+e775cf8c";
4502
+ var version = "2.0.31-dev.2308+34090b3a";
4503
4503
  //#endregion
4504
4504
  //#region src/request.ts
4505
4505
  /**
@@ -5725,6 +5725,50 @@ async function exportMultibaseKey(key) {
5725
5725
  return new TextDecoder().decode(encoded);
5726
5726
  }
5727
5727
  //#endregion
5728
+ //#region src/langtag.ts
5729
+ const EXTLANG_PAIRS = new Set(Object.entries({
5730
+ ar: "aao abh abv acm acq acw acx acy adf aeb aec afb ajp apc apd arb arq ars ary arz auz avl ayh ayl ayn ayp bbz pga shu ssh",
5731
+ kok: "gom knn",
5732
+ lv: "ltg lvs",
5733
+ ms: "bjn btj bve bvu coa dup hji jak jax kvb kvr kxd lce lcf liw max meo mfa mfb min mqg msi mui orn ors pel pse tmw urk vkk vkt xmm zlm zmi zsm",
5734
+ sgn: "ads aed aen afg ajs ase asf asp asq asw bfi bfk bog bqn bqy bvl bzs cds csc csd cse csf csg csl csn csq csr csx doq dse dsl dsz dyl ecs ehs esl esn eso eth fcs fse fsl fss gds gse gsg gsm gss gus hab haf hds hks hos hps hsh hsl icl iks ils inl ins ise isg isr jcs jhs jks jls jos jsl jus kgi kvk lbs lgs lls lsb lsc lsg lsl lsn lso lsp lst lsv lsw lsy lws mdl mfs mre msd msr mzc mzg mzy nbs ncs nsi nsl nsp nsr nzs okl pgz pks prl prz psc psd psg psl pso psp psr pys rib rms rnb rsi rsl rsm rsn sdl sfb sfs sgg sgx slf sls sqk sqs sqx ssp ssr svk swl syy szs tse tsm tsq tss tsy tza ugn ugy ukl uks vgt vsi vsl vsv wbs xki xml xms yds ygs yhs ysl ysm zhk zib zsl",
5735
+ sw: "swc swh",
5736
+ uz: "uzn uzs",
5737
+ zh: "cdo cjy cmn cnp cpx csp czh czo gan hak hnm hsn luh lzh mnp nan sjc wuu yue"
5738
+ }).flatMap(([prefix, extlangs]) => extlangs.split(" ").map((extlang) => `${prefix}-${extlang}`)));
5739
+ const PREFIX_PATTERN = /^[A-Za-z]{2,3}$/;
5740
+ const EXTLANG_PATTERN = /^[A-Za-z]{3}$/;
5741
+ /**
5742
+ * Normalizes a BCP 47 language tag that has an extended language subtag
5743
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
5744
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
5745
+ * tags, so tags should be normalized before being passed to it.
5746
+ *
5747
+ * Only registered extlangs that follow their registered prefix are removed;
5748
+ * any other tag is returned unchanged, so the result still needs to be
5749
+ * validated, e.g., by `Intl.Locale`.
5750
+ *
5751
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
5752
+ * releases. When merging it into the *main* branch, remove the `@internal`
5753
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
5754
+ * the changelog fragment.
5755
+ *
5756
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
5757
+ *
5758
+ * @param tag The BCP 47 language tag to normalize.
5759
+ * @returns The normalized language tag.
5760
+ * @internal
5761
+ */
5762
+ function normalizeLanguageTag(tag) {
5763
+ const subtags = tag.split("-");
5764
+ if (subtags.length < 2) return tag;
5765
+ const [prefix, extlang, ...rest] = subtags;
5766
+ if (!PREFIX_PATTERN.test(prefix) || !EXTLANG_PATTERN.test(extlang)) return tag;
5767
+ const language = extlang.toLowerCase();
5768
+ if (!EXTLANG_PAIRS.has(`${prefix.toLowerCase()}-${language}`)) return tag;
5769
+ return [language, ...rest].join("-");
5770
+ }
5771
+ //#endregion
5728
5772
  //#region src/langstr.ts
5729
5773
  /**
5730
5774
  * A language-tagged string which corresponds to the `rdf:langString` type.
@@ -5739,11 +5783,13 @@ var LanguageString = class extends String {
5739
5783
  * Constructs a new `LanguageString`.
5740
5784
  * @param value A string value written in the given language.
5741
5785
  * @param language The language of the string. If a string is given, it will
5742
- * be parsed as a `Intl.Locale` object.
5786
+ * be parsed as a `Intl.Locale` object. Extended language
5787
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
5788
+ * `yue`.
5743
5789
  */
5744
5790
  constructor(value, language) {
5745
5791
  super(value);
5746
- this.locale = typeof language === "string" ? new Intl.Locale(language) : language;
5792
+ this.locale = typeof language === "string" ? new Intl.Locale(normalizeLanguageTag(language)) : language;
5747
5793
  }
5748
5794
  };
5749
5795
  LanguageString.prototype[Symbol.for("Deno.customInspect")] = function(inspect, options) {
@@ -5753,4 +5799,4 @@ LanguageString.prototype[Symbol.for("nodejs.util.inspect.custom")] = function(_d
5753
5799
  return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`;
5754
5800
  };
5755
5801
  //#endregion
5756
- export { FetchError, LanguageString, UrlError, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, preloadedContexts, validatePublicUrl };
5802
+ export { FetchError, LanguageString, UrlError, createActivityPubRequest, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportMultibaseKey, exportSpki, getDocumentLoader, getRemoteDocument, getUserAgent, importMultibaseKey, importPem, importPkcs1, importSpki, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, normalizeLanguageTag, preloadedContexts, validatePublicUrl };
@@ -1,4 +1,4 @@
1
- import { t as FetchError } from "./request-JVGlYdPG.mjs";
1
+ import { t as FetchError } from "./request-BV7yaWdl.mjs";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-75RoXeN4.cjs");
2
+ const require_request = require("./request-Ban3HXuI.cjs");
3
3
  let _logtape_logtape = require("@logtape/logtape");
4
4
  //#region src/body.ts
5
5
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_body = require("./body-KqP1lgFZ.cjs");
2
+ const require_body = require("./body-DGmcZ1Kz.cjs");
3
3
  let node_assert_strict = require("node:assert/strict");
4
4
  let node_test = require("node:test");
5
5
  let node_zlib = require("node:zlib");
@@ -1,4 +1,4 @@
1
- import { r as readBoundedText, t as BodyTooLargeError } from "./body-CGeu_wlY.mjs";
1
+ import { r as readBoundedText, t as BodyTooLargeError } from "./body-Bx6-vXo_.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
3
  import { test } from "node:test";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
@@ -1,6 +1,6 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-75RoXeN4.cjs");
3
- const require_body = require("./body-KqP1lgFZ.cjs");
2
+ const require_request = require("./request-Ban3HXuI.cjs");
3
+ const require_body = require("./body-DGmcZ1Kz.cjs");
4
4
  const require_link = require("./link-FguCydMA.cjs");
5
5
  const require_url = require("./url-Qk1svKZU.cjs");
6
6
  let node_test = require("node:test");
@@ -1,5 +1,5 @@
1
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-JVGlYdPG.mjs";
2
- import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-CGeu_wlY.mjs";
1
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-BV7yaWdl.mjs";
2
+ import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-Bx6-vXo_.mjs";
3
3
  import { t as HttpHeaderLink } from "./link-NUUWCdnK.mjs";
4
4
  import { o as validatePublicUrl, t as UrlError } from "./url-DzlMS01z.mjs";
5
5
  import { test } from "node:test";
@@ -1,4 +1,5 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
+ const require_langtag = require("./langtag-Bm8oKhIO.cjs");
2
3
  let node_test = require("node:test");
3
4
  let node_assert = require("node:assert");
4
5
  let node_util = require("node:util");
@@ -17,11 +18,13 @@ var LanguageString = class extends String {
17
18
  * Constructs a new `LanguageString`.
18
19
  * @param value A string value written in the given language.
19
20
  * @param language The language of the string. If a string is given, it will
20
- * be parsed as a `Intl.Locale` object.
21
+ * be parsed as a `Intl.Locale` object. Extended language
22
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
23
+ * `yue`.
21
24
  */
22
25
  constructor(value, language) {
23
26
  super(value);
24
- this.locale = typeof language === "string" ? new Intl.Locale(language) : language;
27
+ this.locale = typeof language === "string" ? new Intl.Locale(require_langtag.normalizeLanguageTag(language)) : language;
25
28
  }
26
29
  };
27
30
  LanguageString.prototype[Symbol.for("Deno.customInspect")] = function(inspect, options) {
@@ -38,6 +41,13 @@ LanguageString.prototype[Symbol.for("nodejs.util.inspect.custom")] = function(_d
38
41
  (0, node_assert.deepStrictEqual)(langStr.locale, new Intl.Locale("en"));
39
42
  (0, node_assert.deepStrictEqual)(new LanguageString("Hello", new Intl.Locale("en")), langStr);
40
43
  });
44
+ (0, node_test.test)("new LanguageString() with extlang tags", () => {
45
+ (0, node_assert.deepStrictEqual)(new LanguageString("你好", "zh-YUE").locale.baseName, "yue");
46
+ (0, node_assert.deepStrictEqual)(new LanguageString("你好", "zh-yue-HK").locale.baseName, "yue-HK");
47
+ (0, node_assert.deepStrictEqual)(new LanguageString("你好", "zh-cmn-Hant").locale.baseName, new Intl.Locale("cmn-Hant").baseName);
48
+ (0, node_assert.throws)(() => new LanguageString("Hello", "en-USA"), RangeError);
49
+ (0, node_assert.throws)(() => new LanguageString("你好", "zh-yue-"), RangeError);
50
+ });
41
51
  (0, node_test.test)("Deno.inspect(LanguageString)", () => {
42
52
  const langStr = new LanguageString("Hello, 'world'", "en");
43
53
  (0, node_assert.deepStrictEqual)(node_util.default.inspect(langStr, { colors: false }), "<en> \"Hello, 'world'\"");
@@ -1,5 +1,6 @@
1
+ import { t as normalizeLanguageTag } from "./langtag-QZZgy903.mjs";
1
2
  import { test } from "node:test";
2
- import { deepStrictEqual } from "node:assert";
3
+ import { deepStrictEqual, throws } from "node:assert";
3
4
  import util from "node:util";
4
5
  //#region src/langstr.ts
5
6
  /**
@@ -15,11 +16,13 @@ var LanguageString = class extends String {
15
16
  * Constructs a new `LanguageString`.
16
17
  * @param value A string value written in the given language.
17
18
  * @param language The language of the string. If a string is given, it will
18
- * be parsed as a `Intl.Locale` object.
19
+ * be parsed as a `Intl.Locale` object. Extended language
20
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
21
+ * `yue`.
19
22
  */
20
23
  constructor(value, language) {
21
24
  super(value);
22
- this.locale = typeof language === "string" ? new Intl.Locale(language) : language;
25
+ this.locale = typeof language === "string" ? new Intl.Locale(normalizeLanguageTag(language)) : language;
23
26
  }
24
27
  };
25
28
  LanguageString.prototype[Symbol.for("Deno.customInspect")] = function(inspect, options) {
@@ -36,6 +39,13 @@ test("new LanguageString()", () => {
36
39
  deepStrictEqual(langStr.locale, new Intl.Locale("en"));
37
40
  deepStrictEqual(new LanguageString("Hello", new Intl.Locale("en")), langStr);
38
41
  });
42
+ test("new LanguageString() with extlang tags", () => {
43
+ deepStrictEqual(new LanguageString("你好", "zh-YUE").locale.baseName, "yue");
44
+ deepStrictEqual(new LanguageString("你好", "zh-yue-HK").locale.baseName, "yue-HK");
45
+ deepStrictEqual(new LanguageString("你好", "zh-cmn-Hant").locale.baseName, new Intl.Locale("cmn-Hant").baseName);
46
+ throws(() => new LanguageString("Hello", "en-USA"), RangeError);
47
+ throws(() => new LanguageString("你好", "zh-yue-"), RangeError);
48
+ });
39
49
  test("Deno.inspect(LanguageString)", () => {
40
50
  const langStr = new LanguageString("Hello, 'world'", "en");
41
51
  deepStrictEqual(util.inspect(langStr, { colors: false }), "<en> \"Hello, 'world'\"");
@@ -0,0 +1,50 @@
1
+ //#region src/langtag.ts
2
+ const EXTLANG_PAIRS = new Set(Object.entries({
3
+ ar: "aao abh abv acm acq acw acx acy adf aeb aec afb ajp apc apd arb arq ars ary arz auz avl ayh ayl ayn ayp bbz pga shu ssh",
4
+ kok: "gom knn",
5
+ lv: "ltg lvs",
6
+ ms: "bjn btj bve bvu coa dup hji jak jax kvb kvr kxd lce lcf liw max meo mfa mfb min mqg msi mui orn ors pel pse tmw urk vkk vkt xmm zlm zmi zsm",
7
+ sgn: "ads aed aen afg ajs ase asf asp asq asw bfi bfk bog bqn bqy bvl bzs cds csc csd cse csf csg csl csn csq csr csx doq dse dsl dsz dyl ecs ehs esl esn eso eth fcs fse fsl fss gds gse gsg gsm gss gus hab haf hds hks hos hps hsh hsl icl iks ils inl ins ise isg isr jcs jhs jks jls jos jsl jus kgi kvk lbs lgs lls lsb lsc lsg lsl lsn lso lsp lst lsv lsw lsy lws mdl mfs mre msd msr mzc mzg mzy nbs ncs nsi nsl nsp nsr nzs okl pgz pks prl prz psc psd psg psl pso psp psr pys rib rms rnb rsi rsl rsm rsn sdl sfb sfs sgg sgx slf sls sqk sqs sqx ssp ssr svk swl syy szs tse tsm tsq tss tsy tza ugn ugy ukl uks vgt vsi vsl vsv wbs xki xml xms yds ygs yhs ysl ysm zhk zib zsl",
8
+ sw: "swc swh",
9
+ uz: "uzn uzs",
10
+ zh: "cdo cjy cmn cnp cpx csp czh czo gan hak hnm hsn luh lzh mnp nan sjc wuu yue"
11
+ }).flatMap(([prefix, extlangs]) => extlangs.split(" ").map((extlang) => `${prefix}-${extlang}`)));
12
+ const PREFIX_PATTERN = /^[A-Za-z]{2,3}$/;
13
+ const EXTLANG_PATTERN = /^[A-Za-z]{3}$/;
14
+ /**
15
+ * Normalizes a BCP 47 language tag that has an extended language subtag
16
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
17
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
18
+ * tags, so tags should be normalized before being passed to it.
19
+ *
20
+ * Only registered extlangs that follow their registered prefix are removed;
21
+ * any other tag is returned unchanged, so the result still needs to be
22
+ * validated, e.g., by `Intl.Locale`.
23
+ *
24
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
25
+ * releases. When merging it into the *main* branch, remove the `@internal`
26
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
27
+ * the changelog fragment.
28
+ *
29
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
30
+ *
31
+ * @param tag The BCP 47 language tag to normalize.
32
+ * @returns The normalized language tag.
33
+ * @internal
34
+ */
35
+ function normalizeLanguageTag(tag) {
36
+ const subtags = tag.split("-");
37
+ if (subtags.length < 2) return tag;
38
+ const [prefix, extlang, ...rest] = subtags;
39
+ if (!PREFIX_PATTERN.test(prefix) || !EXTLANG_PATTERN.test(extlang)) return tag;
40
+ const language = extlang.toLowerCase();
41
+ if (!EXTLANG_PAIRS.has(`${prefix.toLowerCase()}-${language}`)) return tag;
42
+ return [language, ...rest].join("-");
43
+ }
44
+ //#endregion
45
+ Object.defineProperty(exports, "normalizeLanguageTag", {
46
+ enumerable: true,
47
+ get: function() {
48
+ return normalizeLanguageTag;
49
+ }
50
+ });
@@ -0,0 +1,45 @@
1
+ //#region src/langtag.ts
2
+ const EXTLANG_PAIRS = new Set(Object.entries({
3
+ ar: "aao abh abv acm acq acw acx acy adf aeb aec afb ajp apc apd arb arq ars ary arz auz avl ayh ayl ayn ayp bbz pga shu ssh",
4
+ kok: "gom knn",
5
+ lv: "ltg lvs",
6
+ ms: "bjn btj bve bvu coa dup hji jak jax kvb kvr kxd lce lcf liw max meo mfa mfb min mqg msi mui orn ors pel pse tmw urk vkk vkt xmm zlm zmi zsm",
7
+ sgn: "ads aed aen afg ajs ase asf asp asq asw bfi bfk bog bqn bqy bvl bzs cds csc csd cse csf csg csl csn csq csr csx doq dse dsl dsz dyl ecs ehs esl esn eso eth fcs fse fsl fss gds gse gsg gsm gss gus hab haf hds hks hos hps hsh hsl icl iks ils inl ins ise isg isr jcs jhs jks jls jos jsl jus kgi kvk lbs lgs lls lsb lsc lsg lsl lsn lso lsp lst lsv lsw lsy lws mdl mfs mre msd msr mzc mzg mzy nbs ncs nsi nsl nsp nsr nzs okl pgz pks prl prz psc psd psg psl pso psp psr pys rib rms rnb rsi rsl rsm rsn sdl sfb sfs sgg sgx slf sls sqk sqs sqx ssp ssr svk swl syy szs tse tsm tsq tss tsy tza ugn ugy ukl uks vgt vsi vsl vsv wbs xki xml xms yds ygs yhs ysl ysm zhk zib zsl",
8
+ sw: "swc swh",
9
+ uz: "uzn uzs",
10
+ zh: "cdo cjy cmn cnp cpx csp czh czo gan hak hnm hsn luh lzh mnp nan sjc wuu yue"
11
+ }).flatMap(([prefix, extlangs]) => extlangs.split(" ").map((extlang) => `${prefix}-${extlang}`)));
12
+ const PREFIX_PATTERN = /^[A-Za-z]{2,3}$/;
13
+ const EXTLANG_PATTERN = /^[A-Za-z]{3}$/;
14
+ /**
15
+ * Normalizes a BCP 47 language tag that has an extended language subtag
16
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
17
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
18
+ * tags, so tags should be normalized before being passed to it.
19
+ *
20
+ * Only registered extlangs that follow their registered prefix are removed;
21
+ * any other tag is returned unchanged, so the result still needs to be
22
+ * validated, e.g., by `Intl.Locale`.
23
+ *
24
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
25
+ * releases. When merging it into the *main* branch, remove the `@internal`
26
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
27
+ * the changelog fragment.
28
+ *
29
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
30
+ *
31
+ * @param tag The BCP 47 language tag to normalize.
32
+ * @returns The normalized language tag.
33
+ * @internal
34
+ */
35
+ function normalizeLanguageTag(tag) {
36
+ const subtags = tag.split("-");
37
+ if (subtags.length < 2) return tag;
38
+ const [prefix, extlang, ...rest] = subtags;
39
+ if (!PREFIX_PATTERN.test(prefix) || !EXTLANG_PATTERN.test(extlang)) return tag;
40
+ const language = extlang.toLowerCase();
41
+ if (!EXTLANG_PAIRS.has(`${prefix.toLowerCase()}-${language}`)) return tag;
42
+ return [language, ...rest].join("-");
43
+ }
44
+ //#endregion
45
+ export { normalizeLanguageTag as t };
@@ -0,0 +1,46 @@
1
+ require("./chunk-Do9eywBl.cjs");
2
+ const require_langtag = require("./langtag-Bm8oKhIO.cjs");
3
+ let node_test = require("node:test");
4
+ let node_assert = require("node:assert");
5
+ //#region src/langtag.test.ts
6
+ (0, node_test.test)("normalizeLanguageTag() removes extlang subtags", () => {
7
+ for (const [input, expected] of [
8
+ ["zh-yue", "yue"],
9
+ ["zh-YUE", "yue"],
10
+ ["ZH-Yue-hk", "yue-hk"],
11
+ ["zh-yue-HK", "yue-HK"],
12
+ ["zh-cmn-Hant", "cmn-Hant"],
13
+ ["zh-yue-u-nu-hanidec", "yue-u-nu-hanidec"],
14
+ ["sgn-ase", "ase"],
15
+ ["ar-arz", "arz"],
16
+ ["ms-zlm", "zlm"],
17
+ ["kok-gom", "gom"],
18
+ ["koK-gom", "gom"],
19
+ ["lv-ltg", "ltg"],
20
+ ["sw-swc", "swc"],
21
+ ["uz-uzs", "uzs"],
22
+ ["zh-yue-cmn", "yue-cmn"],
23
+ ["zh-yue-", "yue-"]
24
+ ]) (0, node_assert.deepStrictEqual)(require_langtag.normalizeLanguageTag(input), expected, input);
25
+ });
26
+ (0, node_test.test)("normalizeLanguageTag() leaves other tags unchanged", () => {
27
+ for (const tag of [
28
+ "",
29
+ "en",
30
+ "en-US",
31
+ "en-USA",
32
+ "en-yue",
33
+ "zh",
34
+ "zh-Hant-TW",
35
+ "yue",
36
+ "yue-HK",
37
+ "x-private",
38
+ "i-klingon",
39
+ "zh-min-nan",
40
+ "koK-gom",
41
+ "zh-yuK",
42
+ "constructor-yue",
43
+ "toString-yue"
44
+ ]) (0, node_assert.deepStrictEqual)(require_langtag.normalizeLanguageTag(tag), tag, tag);
45
+ });
46
+ //#endregion
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,46 @@
1
+ import { t as normalizeLanguageTag } from "./langtag-QZZgy903.mjs";
2
+ import { test } from "node:test";
3
+ import { deepStrictEqual } from "node:assert";
4
+ //#region src/langtag.test.ts
5
+ test("normalizeLanguageTag() removes extlang subtags", () => {
6
+ for (const [input, expected] of [
7
+ ["zh-yue", "yue"],
8
+ ["zh-YUE", "yue"],
9
+ ["ZH-Yue-hk", "yue-hk"],
10
+ ["zh-yue-HK", "yue-HK"],
11
+ ["zh-cmn-Hant", "cmn-Hant"],
12
+ ["zh-yue-u-nu-hanidec", "yue-u-nu-hanidec"],
13
+ ["sgn-ase", "ase"],
14
+ ["ar-arz", "arz"],
15
+ ["ms-zlm", "zlm"],
16
+ ["kok-gom", "gom"],
17
+ ["koK-gom", "gom"],
18
+ ["lv-ltg", "ltg"],
19
+ ["sw-swc", "swc"],
20
+ ["uz-uzs", "uzs"],
21
+ ["zh-yue-cmn", "yue-cmn"],
22
+ ["zh-yue-", "yue-"]
23
+ ]) deepStrictEqual(normalizeLanguageTag(input), expected, input);
24
+ });
25
+ test("normalizeLanguageTag() leaves other tags unchanged", () => {
26
+ for (const tag of [
27
+ "",
28
+ "en",
29
+ "en-US",
30
+ "en-USA",
31
+ "en-yue",
32
+ "zh",
33
+ "zh-Hant-TW",
34
+ "yue",
35
+ "yue-HK",
36
+ "x-private",
37
+ "i-klingon",
38
+ "zh-min-nan",
39
+ "koK-gom",
40
+ "zh-yuK",
41
+ "constructor-yue",
42
+ "toString-yue"
43
+ ]) deepStrictEqual(normalizeLanguageTag(tag), tag, tag);
44
+ });
45
+ //#endregion
46
+ export {};
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.0.31-dev.2306+e775cf8c";
4
+ var version = "2.0.31-dev.2308+34090b3a";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_chunk.__toESM(node_process);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.0.31-dev.2306+e775cf8c";
6
+ var version = "2.0.31-dev.2308+34090b3a";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-75RoXeN4.cjs");
2
+ const require_request = require("./request-Ban3HXuI.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_process = require("node:process");
5
5
  node_process = require_chunk.__toESM(node_process);
@@ -1,4 +1,4 @@
1
- import { o as version, r as getUserAgent } from "./request-JVGlYdPG.mjs";
1
+ import { o as version, r as getUserAgent } from "./request-BV7yaWdl.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.0.31-dev.2306+e775cf8c",
3
+ "version": "2.0.31-dev.2308+34090b3a",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,4 @@
1
- import { deepStrictEqual } from "node:assert";
1
+ import { deepStrictEqual, throws } from "node:assert";
2
2
  import { test } from "node:test";
3
3
  import util from "node:util";
4
4
  import { LanguageString } from "./langstr.ts";
@@ -11,6 +11,23 @@ test("new LanguageString()", () => {
11
11
  deepStrictEqual(new LanguageString("Hello", new Intl.Locale("en")), langStr);
12
12
  });
13
13
 
14
+ test("new LanguageString() with extlang tags", () => {
15
+ // Intl.Locale objects have no own enumerable properties, so their base names
16
+ // are compared instead:
17
+ deepStrictEqual(new LanguageString("你好", "zh-YUE").locale.baseName, "yue");
18
+ deepStrictEqual(
19
+ new LanguageString("你好", "zh-yue-HK").locale.baseName,
20
+ "yue-HK",
21
+ );
22
+ // Whether cmn is canonicalized to zh depends on the runtime:
23
+ deepStrictEqual(
24
+ new LanguageString("你好", "zh-cmn-Hant").locale.baseName,
25
+ new Intl.Locale("cmn-Hant").baseName,
26
+ );
27
+ throws(() => new LanguageString("Hello", "en-USA"), RangeError);
28
+ throws(() => new LanguageString("你好", "zh-yue-"), RangeError);
29
+ });
30
+
14
31
  test("Deno.inspect(LanguageString)", () => {
15
32
  const langStr = new LanguageString("Hello, 'world'", "en");
16
33
  deepStrictEqual(
package/src/langstr.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { normalizeLanguageTag } from "./langtag.ts";
2
+
1
3
  /**
2
4
  * A language-tagged string which corresponds to the `rdf:langString` type.
3
5
  */
@@ -12,12 +14,14 @@ export class LanguageString extends String {
12
14
  * Constructs a new `LanguageString`.
13
15
  * @param value A string value written in the given language.
14
16
  * @param language The language of the string. If a string is given, it will
15
- * be parsed as a `Intl.Locale` object.
17
+ * be parsed as a `Intl.Locale` object. Extended language
18
+ * subtags are normalized first, e.g., `zh-yue` is parsed as
19
+ * `yue`.
16
20
  */
17
21
  constructor(value: string, language: Intl.Locale | string) {
18
22
  super(value);
19
23
  this.locale = typeof language === "string"
20
- ? new Intl.Locale(language)
24
+ ? new Intl.Locale(normalizeLanguageTag(language))
21
25
  : language;
22
26
  }
23
27
  }
@@ -0,0 +1,53 @@
1
+ import { deepStrictEqual } from "node:assert";
2
+ import { test } from "node:test";
3
+ import { normalizeLanguageTag } from "./langtag.ts";
4
+
5
+ test("normalizeLanguageTag() removes extlang subtags", () => {
6
+ const cases: [string, string][] = [
7
+ ["zh-yue", "yue"],
8
+ ["zh-YUE", "yue"],
9
+ ["ZH-Yue-hk", "yue-hk"],
10
+ ["zh-yue-HK", "yue-HK"],
11
+ ["zh-cmn-Hant", "cmn-Hant"],
12
+ ["zh-yue-u-nu-hanidec", "yue-u-nu-hanidec"],
13
+ ["sgn-ase", "ase"],
14
+ ["ar-arz", "arz"],
15
+ ["ms-zlm", "zlm"],
16
+ ["kok-gom", "gom"],
17
+ ["koK-gom", "gom"],
18
+ ["lv-ltg", "ltg"],
19
+ ["sw-swc", "swc"],
20
+ ["uz-uzs", "uzs"],
21
+ // Valid tags never have more than one extlang, so only the first one is
22
+ // removed and the result is still rejected by Intl.Locale:
23
+ ["zh-yue-cmn", "yue-cmn"],
24
+ ["zh-yue-", "yue-"],
25
+ ];
26
+ for (const [input, expected] of cases) {
27
+ deepStrictEqual(normalizeLanguageTag(input), expected, input);
28
+ }
29
+ });
30
+
31
+ test("normalizeLanguageTag() leaves other tags unchanged", () => {
32
+ const tags = [
33
+ "",
34
+ "en",
35
+ "en-US",
36
+ "en-USA",
37
+ "en-yue",
38
+ "zh",
39
+ "zh-Hant-TW",
40
+ "yue",
41
+ "yue-HK",
42
+ "x-private",
43
+ "i-klingon",
44
+ "zh-min-nan",
45
+ "koK-gom",
46
+ "zh-yuK",
47
+ "constructor-yue",
48
+ "toString-yue",
49
+ ];
50
+ for (const tag of tags) {
51
+ deepStrictEqual(normalizeLanguageTag(tag), tag, tag);
52
+ }
53
+ });
package/src/langtag.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Extended language subtags (extlangs) registered in the IANA Language Subtag
3
+ * Registry, grouped by their prefixes. Every extlang record has exactly one
4
+ * prefix and a `Preferred-Value` equal to the extlang subtag itself, so
5
+ * replacing the prefix and the extlang with the extlang alone yields
6
+ * the canonical form.
7
+ *
8
+ * Generated from the registry with File-Date: 2026-09-17.
9
+ */
10
+ const EXTLANGS: Record<string, string> = {
11
+ ar: "aao abh abv acm acq acw acx acy adf aeb aec afb ajp apc apd arb arq " +
12
+ "ars ary arz auz avl ayh ayl ayn ayp bbz pga shu ssh",
13
+ kok: "gom knn",
14
+ lv: "ltg lvs",
15
+ ms: "bjn btj bve bvu coa dup hji jak jax kvb kvr kxd lce lcf liw max meo " +
16
+ "mfa mfb min mqg msi mui orn ors pel pse tmw urk vkk vkt xmm zlm zmi " +
17
+ "zsm",
18
+ sgn: "ads aed aen afg ajs ase asf asp asq asw bfi bfk bog bqn bqy bvl bzs " +
19
+ "cds csc csd cse csf csg csl csn csq csr csx doq dse dsl dsz dyl ecs " +
20
+ "ehs esl esn eso eth fcs fse fsl fss gds gse gsg gsm gss gus hab haf " +
21
+ "hds hks hos hps hsh hsl icl iks ils inl ins ise isg isr jcs jhs jks " +
22
+ "jls jos jsl jus kgi kvk lbs lgs lls lsb lsc lsg lsl lsn lso lsp lst " +
23
+ "lsv lsw lsy lws mdl mfs mre msd msr mzc mzg mzy nbs ncs nsi nsl nsp " +
24
+ "nsr nzs okl pgz pks prl prz psc psd psg psl pso psp psr pys rib rms " +
25
+ "rnb rsi rsl rsm rsn sdl sfb sfs sgg sgx slf sls sqk sqs sqx ssp ssr " +
26
+ "svk swl syy szs tse tsm tsq tss tsy tza ugn ugy ukl uks vgt vsi vsl " +
27
+ "vsv wbs xki xml xms yds ygs yhs ysl ysm zhk zib zsl",
28
+ sw: "swc swh",
29
+ uz: "uzn uzs",
30
+ zh: "cdo cjy cmn cnp cpx csp czh czo gan hak hnm hsn luh lzh mnp nan sjc " +
31
+ "wuu yue",
32
+ };
33
+
34
+ const EXTLANG_PAIRS: ReadonlySet<string> = new Set(
35
+ Object.entries(EXTLANGS).flatMap(([prefix, extlangs]) =>
36
+ extlangs.split(" ").map((extlang) => `${prefix}-${extlang}`)
37
+ ),
38
+ );
39
+
40
+ const PREFIX_PATTERN = /^[A-Za-z]{2,3}$/;
41
+ const EXTLANG_PATTERN = /^[A-Za-z]{3}$/;
42
+
43
+ /**
44
+ * Normalizes a BCP 47 language tag that has an extended language subtag
45
+ * (extlang), such as `zh-yue`, into its canonical form, such as `yue`, as
46
+ * described in [RFC 5646, Section 4.5][1]. `Intl.Locale` rejects extlang
47
+ * tags, so tags should be normalized before being passed to it.
48
+ *
49
+ * Only registered extlangs that follow their registered prefix are removed;
50
+ * any other tag is returned unchanged, so the result still needs to be
51
+ * validated, e.g., by `Intl.Locale`.
52
+ *
53
+ * NOTE: This function is marked as internal in the 2.0.x–2.4.x patch
54
+ * releases. When merging it into the *main* branch, remove the `@internal`
55
+ * tag, formalize it as a public API (`@since 2.5.0`), and add it to
56
+ * the changelog fragment.
57
+ *
58
+ * [1]: https://www.rfc-editor.org/rfc/rfc5646.html#section-4.5
59
+ *
60
+ * @param tag The BCP 47 language tag to normalize.
61
+ * @returns The normalized language tag.
62
+ * @internal
63
+ */
64
+ export function normalizeLanguageTag(tag: string): string {
65
+ const subtags = tag.split("-");
66
+ if (subtags.length < 2) return tag;
67
+ const [prefix, extlang, ...rest] = subtags;
68
+ // Checked before case folding, since `toLowerCase()` maps some non-ASCII
69
+ // characters (e.g., U+212A KELVIN SIGN) to ASCII letters:
70
+ if (!PREFIX_PATTERN.test(prefix) || !EXTLANG_PATTERN.test(extlang)) {
71
+ return tag;
72
+ }
73
+ const language = extlang.toLowerCase();
74
+ if (!EXTLANG_PAIRS.has(`${prefix.toLowerCase()}-${language}`)) return tag;
75
+ return [language, ...rest].join("-");
76
+ }
package/src/mod.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  importSpki,
26
26
  } from "./key.ts";
27
27
  export { LanguageString } from "./langstr.ts";
28
+ export { normalizeLanguageTag } from "./langtag.ts";
28
29
  export {
29
30
  decodeMultibase,
30
31
  encodeMultibase,