summaryrefslogtreecommitdiff
path: root/node_modules/bcp-47-normalize/lib/index.js
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
committerShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
commit7a52ddeba2a68388b544f529d2d92104420f77b0 (patch)
tree15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/bcp-47-normalize/lib/index.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/bcp-47-normalize/lib/index.js')
-rw-r--r--node_modules/bcp-47-normalize/lib/index.js335
1 files changed, 335 insertions, 0 deletions
diff --git a/node_modules/bcp-47-normalize/lib/index.js b/node_modules/bcp-47-normalize/lib/index.js
new file mode 100644
index 0000000..3d61b7e
--- /dev/null
+++ b/node_modules/bcp-47-normalize/lib/index.js
@@ -0,0 +1,335 @@
+/**
+ * @typedef {import('bcp-47').Warning} Warning
+ * @typedef {import('bcp-47').Schema} Schema
+ * @typedef {import('bcp-47').Extension} Extension
+ *
+ * @typedef Options
+ * Configuration (optional).
+ * @property {boolean} [forgiving]
+ * Passed to `bcp-47` as `options.forgiving`.
+ * @property {Warning} [warning]
+ * Passed to `bcp-47` as `options.warning`.
+ *
+ * One additional warning is given:
+ *
+ * | code | reason |
+ * | :--- | :--------------------------------------------------------- |
+ * | 7 | Deprecated region `CURRENT`, expected one of `SUGGESTIONS` |
+ *
+ * This warning is only given if the region cannot be automatically fixed
+ * (when regions split into multiple regions).
+ */
+
+import {parse, stringify} from 'bcp-47'
+import {extendedFilter} from 'bcp-47-match'
+import {matches} from './matches.js'
+import {fields} from './fields.js'
+import {many} from './many.js'
+import {likely} from './likely.js'
+
+const own = {}.hasOwnProperty
+
+/**
+ * @param {Schema} base
+ * @param {Partial<Schema>} changes
+ * @returns {Schema}
+ */
+function merge(base, changes) {
+ if (!base.language) base.language = changes.language
+ if (!base.script) base.script = changes.script
+ if (!base.region) base.region = changes.region
+ if (changes.variants) base.variants.push(...changes.variants)
+
+ return base
+}
+
+/**
+ * Mostly like:
+ * <https://github.com/formatjs/formatjs/blob/a15e757/packages/intl-locale/index.ts#L254>
+ * But doesn’t crash.
+ *
+ * @param {Schema} schema
+ * @returns {string}
+ */
+function addLikelySubtags(schema) {
+ const {language, script, region} = schema
+ /** @type {string|undefined} */
+ let match
+
+ if (
+ script &&
+ region &&
+ (match = likely[stringify({language, script, region})])
+ ) {
+ schema.script = undefined
+ schema.region = undefined
+ } else if (script && (match = likely[stringify({language, script})])) {
+ schema.script = undefined
+ } else if (region && (match = likely[stringify({language, region})])) {
+ schema.region = undefined
+ } else if (language && (match = likely[language])) {
+ // Empty.
+ }
+
+ if (match) {
+ schema.language = undefined
+ merge(schema, parse(match))
+ }
+
+ return stringify(schema)
+}
+
+/**
+ * @param {Schema} schema
+ */
+function removeLikelySubtags(schema) {
+ addLikelySubtags(schema)
+
+ const {language, script, region} = schema
+
+ if (!language) return schema
+
+ const maxLocale = stringify({language, script, region})
+
+ if (maxLocale === addLikelySubtags(parse(language))) {
+ schema.script = undefined
+ schema.region = undefined
+ } else if (
+ region &&
+ maxLocale === addLikelySubtags(parse(language + '-' + region))
+ ) {
+ schema.script = undefined
+ } else if (
+ script &&
+ maxLocale === addLikelySubtags(parse(language + '-' + script))
+ ) {
+ schema.region = undefined
+ }
+
+ return schema
+}
+
+/**
+ * Normalize the given BCP 47 tag according to Unicode CLDR suggestions.
+ *
+ * @param {string} tag
+ * BCP 47 tag.
+ * @param {Options} [options]
+ * Configuration (optional).
+ * @returns {string}
+ * Normal, canonical, and pretty BCP 47 tag.
+ */
+export function bcp47Normalize(tag, options) {
+ const settings = options || {}
+ // 1. normalize and lowercase the tag (`sgn-be-fr` -> `sfb`).
+ const schema = parse(String(tag || '').toLowerCase(), settings)
+ const value = stringify(schema)
+
+ if (!value) {
+ return value
+ }
+
+ let index = -1
+
+ // 2. Do fancy, expensive replaces (`ha-latn-gh` -> `ha-gh`).
+ while (++index < matches.length) {
+ let from = matches[index].from
+
+ if (from.slice(0, 4) === 'und-' && schema.language) {
+ from = schema.language + from.slice(3)
+ }
+
+ if (extendedFilter(value, from).length > 0) {
+ replace(schema, from, matches[index].to)
+ }
+ }
+
+ // 3. Do basic field replaces (`en-840` -> `en-us`).
+ index = -1
+
+ while (++index < fields.length) {
+ if (remove(schema, fields[index].from.field, fields[index].from.value)) {
+ add(schema, fields[index].to.field, fields[index].to.value)
+ }
+ }
+
+ // 4. Minimize.
+ removeLikelySubtags(schema)
+
+ // 5. Sort variants, and sort extensions on singleton.
+ schema.variants.sort()
+ schema.extensions.sort(compareSingleton)
+
+ // 6. Warn if fields (currently only regions) should be updated but have
+ // multiple choices.
+ if (settings.warning) {
+ /** @type {keyof many} */
+ let key
+
+ for (key in many) {
+ if (own.call(many, key)) {
+ const map = many[key]
+ const value = schema[key]
+ if (value && own.call(map, value)) {
+ const replacements = map[value]
+ settings.warning(
+ 'Deprecated ' +
+ key +
+ ' `' +
+ value +
+ '`, expected one of `' +
+ replacements.join('`, `') +
+ '`',
+ -1,
+ 7
+ )
+ }
+ }
+ }
+ }
+
+ // 7. Add proper casing back.
+ // Format script (ISO 15924) as titlecase (example: `Latn`):
+ if (schema.script) {
+ schema.script =
+ schema.script.charAt(0).toUpperCase() + schema.script.slice(1)
+ }
+
+ // Format region (ISO 3166) as uppercase (note: this doesn’t affect numeric
+ // codes, which is fine):
+ if (schema.region) {
+ schema.region = schema.region.toUpperCase()
+ }
+
+ return stringify(schema)
+}
+
+/**
+ * @param {Schema} schema
+ * @param {string} from
+ * @param {string} to
+ * @returns {void}
+ */
+function replace(schema, from, to) {
+ const left = parse(from)
+ const right = parse(to)
+ /** @type {Array<string>} */
+ const removed = []
+ /** @type {string|null|undefined} */
+ const lang = left.language
+ /** @type {keyof schema} */
+ let key
+
+ // Remove values from `from`:
+ for (key in left) {
+ if (own.call(left, key)) {
+ const value = left[key]
+ if (value && remove(schema, key, value)) {
+ removed.push(key)
+ }
+ }
+ }
+
+ // Add values from `to`:
+ for (key in right) {
+ if (own.call(right, key)) {
+ const value = right[key]
+ // Only add values that are defined on `to`, and that were either removed by
+ // `from` or are currently empty.
+ if (lang && value && (removed.includes(key) || !schema[key])) {
+ add(schema, key, key === 'language' && value === 'und' ? lang : value)
+ }
+ }
+ }
+}
+
+/**
+ * @param {Schema} object
+ * @param {keyof Schema} key
+ * @param {string|Array<string>|Array<Extension>} value
+ * @returns {boolean}
+ */
+function remove(object, key, value) {
+ let removed = false
+ /** @type {string|Array<string>|Array<Extension>|null|undefined} */
+ let result
+
+ if (value) {
+ const current = object[key]
+ result = current
+
+ if (Array.isArray(current)) {
+ result = []
+ let index = -1
+
+ while (++index < current.length) {
+ const item = current[index]
+
+ // @ts-expect-error: TS can’t handle the two lists.
+ if (value.includes(item)) {
+ removed = true
+ } else {
+ // @ts-expect-error: TS can’t handle the two lists.
+ result.push(item)
+ }
+ }
+ } else if (current === value) {
+ result = null
+ removed = true
+ }
+
+ // @ts-expect-error: Assume the value matches.
+ object[key] = result
+ }
+
+ return removed
+}
+
+/**
+ * @param {Schema} object
+ * @param {keyof Schema} key
+ * @param {string|Array<string>|Array<Extension>} value
+ * @returns {void}
+ */
+function add(object, key, value) {
+ /** @type {string|Array<string>|Array<Extension>|null|undefined} */
+ const current = object[key]
+
+ if (Array.isArray(current)) {
+ const list = Array.isArray(value) ? value : [value]
+ /** @type {number} */
+ let index = -1
+
+ while (++index < list.length) {
+ const item = list[index]
+
+ // @ts-expect-error: TS can’t handle the two lists.
+ if (!current.includes(item)) {
+ // @ts-expect-error: TS can’t handle the two lists.
+ current.push(item)
+ }
+ }
+ } else {
+ // @ts-expect-error: Assume the value matches.
+ object[key] = value
+ }
+}
+
+/**
+ * @param {Extension} left
+ * @param {Extension} right
+ * @returns {number}
+ */
+function compareSingleton(left, right) {
+ if (left.singleton > right.singleton) {
+ return 1
+ }
+
+ if (left.singleton < right.singleton) {
+ return -1
+ }
+
+ // It is invalid to have more than one extension with the same singleton so
+ // we should never reach this code.
+ return 0
+}