199 lines
4.6 KiB
JavaScript
199 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)
|
|||
|
}
|