198 lines
4.6 KiB
JavaScript
198 lines
4.6 KiB
JavaScript
'use strict'
|
||
|
||
var bcp47 = require('bcp-47')
|
||
var match = require('bcp-47-match')
|
||
var matches = require('./matches.json')
|
||
var fields = require('./fields.json')
|
||
var defaults = require('./defaults.json')
|
||
var many = require('./many.json')
|
||
|
||
module.exports = normalize
|
||
|
||
var own = {}.hasOwnProperty
|
||
|
||
var collator = new Intl.Collator()
|
||
|
||
var emptyExtraFields = {
|
||
variants: [],
|
||
extensions: [],
|
||
privateuse: [],
|
||
irregular: null,
|
||
regular: null
|
||
}
|
||
|
||
function normalize(value, options) {
|
||
var settings = options || {}
|
||
// 1. normalize and lowercase the tag (`sgn-be-fr` -> `sfb`).
|
||
var schema = bcp47.parse(String(value || '').toLowerCase(), settings)
|
||
var tag = bcp47.stringify(schema)
|
||
var index = -1
|
||
var key
|
||
|
||
if (!tag) {
|
||
return tag
|
||
}
|
||
|
||
// 2. Do fancy, expensive replaces (`ha-latn-gh` -> `ha-gh`).
|
||
while (++index < matches.length) {
|
||
if (match.extendedFilter(tag, matches[index].from).length) {
|
||
replace(schema, matches[index].from, matches[index].to)
|
||
tag = bcp47.stringify(schema)
|
||
}
|
||
}
|
||
|
||
// 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. Remove defaults (`nl-nl` -> `nl`).
|
||
tag = bcp47.stringify(Object.assign({}, schema, emptyExtraFields))
|
||
index = -1
|
||
|
||
while (++index < defaults.length) {
|
||
if (tag === defaults[index]) {
|
||
replace(
|
||
schema,
|
||
defaults[index],
|
||
defaults[index].split('-').slice(0, -1).join('-')
|
||
)
|
||
tag = bcp47.stringify(Object.assign({}, schema, emptyExtraFields))
|
||
}
|
||
}
|
||
|
||
// 5. Sort extensions on singleton.
|
||
schema.extensions.sort(compareSingleton)
|
||
|
||
// 6. Warn if fields (currently only regions) should be updated but have
|
||
// multiple choices.
|
||
if (settings.warning) {
|
||
for (key in many) {
|
||
if (own.call(many[key], schema[key])) {
|
||
settings.warning(
|
||
'Deprecated ' +
|
||
key +
|
||
' `' +
|
||
schema[key] +
|
||
'`, expected one of `' +
|
||
many[key][schema[key]].join('`, `') +
|
||
'`',
|
||
null,
|
||
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 bcp47.stringify(schema)
|
||
}
|
||
|
||
function replace(schema, from, to) {
|
||
var left = bcp47.parse(from)
|
||
var right = bcp47.parse(to)
|
||
var removed = []
|
||
var key
|
||
|
||
// Remove values from `from`:
|
||
for (key in left) {
|
||
if (left[key] && left[key].length && remove(schema, key, left[key])) {
|
||
removed.push(key)
|
||
}
|
||
}
|
||
|
||
// Add values from `to`:
|
||
for (key in right) {
|
||
// Only add values that are defined on `to`, and that were either removed by
|
||
// `from` or are currently empty.
|
||
if (
|
||
right[key] &&
|
||
right[key].length &&
|
||
(removed.indexOf(key) > -1 || !schema[key] || !schema[key].length)
|
||
) {
|
||
add(schema, key, right[key])
|
||
}
|
||
}
|
||
}
|
||
|
||
function remove(object, key, value) {
|
||
var removed = false
|
||
var current
|
||
var result
|
||
var index
|
||
var item
|
||
|
||
/* istanbul ignore else - this is currently done by wrapping code, so else is
|
||
* never reached.
|
||
* However, that could change in the future, so leave this guard here. */
|
||
if (value) {
|
||
current = object[key]
|
||
result = current
|
||
|
||
if (current && typeof current === 'object') {
|
||
result = []
|
||
index = -1
|
||
|
||
while (++index < current.length) {
|
||
item = current[index]
|
||
|
||
if (value.indexOf(item) < 0) {
|
||
result.push(item)
|
||
} else {
|
||
removed = true
|
||
}
|
||
}
|
||
} else if (current === value) {
|
||
result = null
|
||
removed = true
|
||
}
|
||
|
||
object[key] = result
|
||
}
|
||
|
||
return removed
|
||
}
|
||
|
||
function add(object, key, value) {
|
||
var current = object[key]
|
||
var list
|
||
var index
|
||
var item
|
||
|
||
if (current && typeof current === 'object') {
|
||
list = [].concat(value)
|
||
index = -1
|
||
|
||
while (++index < list.length) {
|
||
item = list[index]
|
||
|
||
/* istanbul ignore else - this currently can’t happen, but guard for the
|
||
* future. */
|
||
if (current.indexOf(item) < 0) {
|
||
current.push(item)
|
||
}
|
||
}
|
||
} else {
|
||
object[key] = value
|
||
}
|
||
}
|
||
|
||
function compareSingleton(left, right) {
|
||
return collator.compare(left.singleton, right.singleton)
|
||
}
|