@fgv/ts-bcp47 5.1.0-2 → 5.1.0-4
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/data/bcp/overrides.json +21 -0
- package/dist/data/unsd/m49.json +3723 -0
- package/dist/index.browser.js +30 -0
- package/dist/index.js +28 -0
- package/dist/packlets/bcp47/bcp47Subtags/converters.js +32 -0
- package/dist/packlets/bcp47/bcp47Subtags/index.js +26 -0
- package/dist/packlets/bcp47/bcp47Subtags/model.js +23 -0
- package/dist/packlets/bcp47/bcp47Subtags/validate.js +48 -0
- package/dist/packlets/bcp47/common.js +53 -0
- package/dist/packlets/bcp47/helpers.js +96 -0
- package/dist/packlets/bcp47/index.js +32 -0
- package/dist/packlets/bcp47/languageRegistryData.js +202 -0
- package/dist/packlets/bcp47/languageTag.js +363 -0
- package/dist/packlets/bcp47/languageTagParser.js +275 -0
- package/dist/packlets/bcp47/match/chooser.js +88 -0
- package/dist/packlets/bcp47/match/common.js +49 -0
- package/dist/packlets/bcp47/match/index.js +26 -0
- package/dist/packlets/bcp47/match/similarity.js +205 -0
- package/dist/packlets/bcp47/normalization/baseNormalizer.js +89 -0
- package/dist/packlets/bcp47/normalization/canonicalNormalizer.js +86 -0
- package/dist/packlets/bcp47/normalization/common.js +77 -0
- package/dist/packlets/bcp47/normalization/index.js +27 -0
- package/dist/packlets/bcp47/normalization/normalizeTag.js +101 -0
- package/dist/packlets/bcp47/normalization/preferredTagNormalizer.js +177 -0
- package/dist/packlets/bcp47/overrides/converters.js +49 -0
- package/dist/packlets/bcp47/overrides/defaultRegistries.js +38 -0
- package/dist/packlets/bcp47/overrides/index.js +25 -0
- package/dist/packlets/bcp47/overrides/model.js +23 -0
- package/dist/packlets/bcp47/overrides/overridesRegistry.js +83 -0
- package/dist/packlets/bcp47/validation/baseValidator.js +86 -0
- package/dist/packlets/bcp47/validation/common.js +77 -0
- package/dist/packlets/bcp47/validation/index.js +29 -0
- package/dist/packlets/bcp47/validation/isCanonical.js +79 -0
- package/dist/packlets/bcp47/validation/isInPreferredForm.js +46 -0
- package/dist/packlets/bcp47/validation/isStrictlyValid.js +94 -0
- package/dist/packlets/bcp47/validation/isValid.js +92 -0
- package/dist/packlets/bcp47/validation/isWellFormed.js +75 -0
- package/dist/packlets/bcp47/validation/validateTag.js +153 -0
- package/dist/packlets/iana/common/converters.js +58 -0
- package/dist/packlets/iana/common/model.js +23 -0
- package/dist/packlets/iana/common/registeredItems.js +120 -0
- package/dist/packlets/iana/common/utils.js +30 -0
- package/dist/packlets/iana/common/validate.js +59 -0
- package/dist/packlets/iana/converters.js +25 -0
- package/dist/packlets/iana/defaultRegistries.js +38 -0
- package/dist/packlets/iana/iana-data-embedded.js +60 -0
- package/dist/packlets/iana/index.browser.js +34 -0
- package/dist/packlets/iana/index.js +34 -0
- package/dist/packlets/iana/jar/converters.js +25 -0
- package/dist/packlets/iana/jar/index.js +26 -0
- package/dist/packlets/iana/jar/jarConverters.js +60 -0
- package/dist/packlets/iana/jar/jarModel.js +23 -0
- package/dist/packlets/iana/jar/language-subtags/converters.js +25 -0
- package/dist/packlets/iana/jar/language-subtags/index.js +26 -0
- package/dist/packlets/iana/jar/language-subtags/model.js +25 -0
- package/dist/packlets/iana/jar/language-subtags/registry/converters.js +180 -0
- package/dist/packlets/iana/jar/language-subtags/registry/index.js +26 -0
- package/dist/packlets/iana/jar/language-subtags/registry/model.js +43 -0
- package/dist/packlets/iana/jar/language-subtags/tags/converters.js +101 -0
- package/dist/packlets/iana/jar/language-subtags/tags/index.js +27 -0
- package/dist/packlets/iana/jar/language-subtags/tags/model.js +23 -0
- package/dist/packlets/iana/jar/language-subtags/tags/tagValidation.js +66 -0
- package/dist/packlets/iana/jar/language-subtags/tags/validate.js +85 -0
- package/dist/packlets/iana/jar/model.js +25 -0
- package/dist/packlets/iana/language-subtags/common.js +23 -0
- package/dist/packlets/iana/language-subtags/converters.js +182 -0
- package/dist/packlets/iana/language-subtags/index.browser.js +30 -0
- package/dist/packlets/iana/language-subtags/index.js +29 -0
- package/dist/packlets/iana/language-subtags/jarConverters.js +288 -0
- package/dist/packlets/iana/language-subtags/model.js +23 -0
- package/dist/packlets/iana/language-subtags/scope.js +169 -0
- package/dist/packlets/iana/language-subtags/subtagRegistry.js +108 -0
- package/dist/packlets/iana/language-subtags/validate.js +23 -0
- package/dist/packlets/iana/language-tag-extensions/converters.js +59 -0
- package/dist/packlets/iana/language-tag-extensions/extensionsRegistry.js +64 -0
- package/dist/packlets/iana/language-tag-extensions/extensionsScope.js +50 -0
- package/dist/packlets/iana/language-tag-extensions/index.js +28 -0
- package/dist/packlets/iana/language-tag-extensions/jarConverters.js +143 -0
- package/dist/packlets/iana/language-tag-extensions/model.js +24 -0
- package/dist/packlets/iana/language-tag-extensions/validate.js +33 -0
- package/dist/packlets/iana/languageRegistries.js +80 -0
- package/dist/packlets/iana/languageRegistriesFileLoader.js +73 -0
- package/dist/packlets/iana/languageRegistriesLoader.js +113 -0
- package/dist/packlets/iana/model.js +25 -0
- package/dist/packlets/iana/validate.js +23 -0
- package/dist/packlets/unsd/areas.js +92 -0
- package/dist/packlets/unsd/common.js +23 -0
- package/dist/packlets/unsd/csv/converters.js +75 -0
- package/dist/packlets/unsd/csv/index.js +25 -0
- package/dist/packlets/unsd/csv/model.js +23 -0
- package/dist/packlets/unsd/defaultRegistries.js +38 -0
- package/dist/packlets/unsd/index.js +27 -0
- package/dist/packlets/unsd/regionCodes.js +115 -0
- package/dist/packlets/unsd/regions.js +71 -0
- package/dist/packlets/utils/index.js +24 -0
- package/dist/packlets/utils/jsonHelpers.js +25 -0
- package/dist/packlets/utils/public.js +24 -0
- package/dist/packlets/utils/validationHelpers.js +116 -0
- package/dist/test/unit/bcp47/commonTestCases.js +475 -0
- package/dist/test/unit/bcp47/languageTagHelpers.js +178 -0
- package/dist/test/unit/iana/testConstants.js +68 -0
- package/dist/tsdoc-metadata.json +1 -1
- package/package.json +36 -31
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2022 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import * as Iana from '../iana';
|
|
23
|
+
import { captureResult, succeed } from '@fgv/ts-utils';
|
|
24
|
+
import { UndeterminedLanguage, subtagsToString } from './common';
|
|
25
|
+
import { mostNormalized } from './normalization/common';
|
|
26
|
+
import { mostValid } from './validation/common';
|
|
27
|
+
import { LanguageRegistryData } from './languageRegistryData';
|
|
28
|
+
import { LanguageTagParser } from './languageTagParser';
|
|
29
|
+
import { NormalizeTag } from './normalization';
|
|
30
|
+
import { ValidateTag } from './validation';
|
|
31
|
+
/**
|
|
32
|
+
* Represents a single BCP-47 language tag.
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export class LanguageTag {
|
|
36
|
+
/**
|
|
37
|
+
* Constructs a {@link Bcp47.LanguageTag | LanguageTag }.
|
|
38
|
+
* @param subtags - The {@link Bcp47.Subtags | subtags } from
|
|
39
|
+
* which the tag is constructed.
|
|
40
|
+
* @param validity - Known {@link Bcp47.TagValidity | validation level} of the
|
|
41
|
+
* supplied subtags.
|
|
42
|
+
* @param normalization - Known {@link Bcp47.TagNormalization | normalization level}
|
|
43
|
+
* of the supplied subtags.
|
|
44
|
+
* @param iana - The {@link Iana.LanguageRegistries} used to validate and normalize
|
|
45
|
+
* this tag.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
constructor(subtags, validity, normalization, iana) {
|
|
49
|
+
this.subtags = Object.freeze(Object.assign({}, subtags));
|
|
50
|
+
this._normalization = normalization;
|
|
51
|
+
this._validity = validity;
|
|
52
|
+
this.tag = subtagsToString(subtags);
|
|
53
|
+
this._iana = iana;
|
|
54
|
+
this.registry = new LanguageRegistryData(this.subtags, iana);
|
|
55
|
+
if (validity === 'strictly-valid') {
|
|
56
|
+
this._isStrictlyValid = true;
|
|
57
|
+
this._isValid = true;
|
|
58
|
+
}
|
|
59
|
+
else if (validity === 'valid') {
|
|
60
|
+
this._isValid = true;
|
|
61
|
+
}
|
|
62
|
+
if (normalization === 'preferred') {
|
|
63
|
+
this._isPreferred = true;
|
|
64
|
+
this._isCanonical = true;
|
|
65
|
+
}
|
|
66
|
+
else if (normalization === 'canonical') {
|
|
67
|
+
this._isCanonical = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The effective script of this language tag, if known.
|
|
72
|
+
* The effective script is the script subtag in the tag itself,
|
|
73
|
+
* if present, or the `Suppress-Script` defined in the
|
|
74
|
+
* {@link https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry | IANA subtag registry}
|
|
75
|
+
* for the primary language of this tag. Can be `undefined`
|
|
76
|
+
* if neither the tag nor the IANA registry define a script.
|
|
77
|
+
*/
|
|
78
|
+
get effectiveScript() {
|
|
79
|
+
var _a;
|
|
80
|
+
return (_a = this.subtags.script) !== null && _a !== void 0 ? _a : this.getSuppressedScript();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Determines if this tag represents the special `undetermined` language.
|
|
84
|
+
*/
|
|
85
|
+
get isUndetermined() {
|
|
86
|
+
var _a;
|
|
87
|
+
/* c8 ignore next */
|
|
88
|
+
return ((_a = this.subtags.primaryLanguage) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === UndeterminedLanguage;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether this language tag is valid.
|
|
92
|
+
*/
|
|
93
|
+
get isValid() {
|
|
94
|
+
if (this._isValid === undefined) {
|
|
95
|
+
this._isValid = ValidateTag.isValid(this.subtags);
|
|
96
|
+
if (this._isValid) {
|
|
97
|
+
this._validity = 'valid';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return this._isValid === true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether if this language tag is strictly valid.
|
|
104
|
+
*/
|
|
105
|
+
get isStrictlyValid() {
|
|
106
|
+
if (this._isStrictlyValid === undefined) {
|
|
107
|
+
this._isStrictlyValid = ValidateTag.isStrictlyValid(this.subtags);
|
|
108
|
+
if (this._isStrictlyValid) {
|
|
109
|
+
this._validity = 'strictly-valid';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return this._isStrictlyValid === true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether this language tag is in canonical form.
|
|
116
|
+
*/
|
|
117
|
+
get isCanonical() {
|
|
118
|
+
if (this._isCanonical === undefined) {
|
|
119
|
+
this._isCanonical = ValidateTag.isCanonical(this.subtags);
|
|
120
|
+
if (this._isCanonical) {
|
|
121
|
+
this._normalization = 'canonical';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return this._isCanonical === true;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Whether this language tag is in preferred form.
|
|
128
|
+
*/
|
|
129
|
+
get isPreferred() {
|
|
130
|
+
if (this._isPreferred === undefined) {
|
|
131
|
+
this._isPreferred = ValidateTag.isInPreferredForm(this.subtags);
|
|
132
|
+
if (this._isPreferred) {
|
|
133
|
+
this._normalization = 'preferred';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return this._isPreferred === true;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether this language tag is a grandfathered tag.
|
|
140
|
+
*/
|
|
141
|
+
get isGrandfathered() {
|
|
142
|
+
return this.subtags.grandfathered !== undefined;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Gets a text description of this tag.
|
|
146
|
+
*/
|
|
147
|
+
get description() {
|
|
148
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
149
|
+
const parts = [];
|
|
150
|
+
if (!this.subtags.grandfathered) {
|
|
151
|
+
if (this.registry.primaryLanguage || this.subtags.primaryLanguage) {
|
|
152
|
+
parts.push(((_b = (_a = this.registry.primaryLanguage) === null || _a === void 0 ? void 0 : _a.description[0]) !== null && _b !== void 0 ? _b : this.subtags.primaryLanguage));
|
|
153
|
+
}
|
|
154
|
+
if (this.registry.extlangs) {
|
|
155
|
+
for (const e of this.registry.extlangs) {
|
|
156
|
+
parts.push(`/ ${(_d = (_c = e.registry) === null || _c === void 0 ? void 0 : _c.description[0]) !== null && _d !== void 0 ? _d : e.subtag}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (this.subtags.script) {
|
|
160
|
+
parts.push(`in ${(_f = (_e = this.registry.script) === null || _e === void 0 ? void 0 : _e.description[0]) !== null && _f !== void 0 ? _f : this.subtags.script} script`);
|
|
161
|
+
}
|
|
162
|
+
if (this.subtags.region) {
|
|
163
|
+
parts.push(`as spoken in ${(_h = (_g = this.registry.region) === null || _g === void 0 ? void 0 : _g.description[0]) !== null && _h !== void 0 ? _h : this.subtags.region}`);
|
|
164
|
+
}
|
|
165
|
+
if (this.registry.variants && this.registry.variants.length > 0) {
|
|
166
|
+
for (const e of this.registry.variants) {
|
|
167
|
+
parts.push(`(${(_k = (_j = e.registry) === null || _j === void 0 ? void 0 : _j.description[0]) !== null && _k !== void 0 ? _k : e.subtag})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (this.registry.extensions && this.registry.extensions.length > 0) {
|
|
171
|
+
for (const e of this.registry.extensions) {
|
|
172
|
+
parts.push(`(${(_m = (_l = e.registry) === null || _l === void 0 ? void 0 : _l.description[0]) !== null && _m !== void 0 ? _m : `-${e.singleton}`} "${e.value}")`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (this.subtags.privateUse && this.subtags.privateUse.length > 0) {
|
|
176
|
+
parts.push(`(-x "${this.subtags.privateUse.join('-')}")`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
parts.push(`${this.tag} (grandfathered)`);
|
|
181
|
+
}
|
|
182
|
+
return parts.join(' ');
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Creates a new {@link Bcp47.LanguageTag | language tag} from a supplied `string` tag
|
|
186
|
+
* using optional configuration, if supplied.
|
|
187
|
+
* @param tag - The `string` tag from which the {@link Bcp47.LanguageTag | language tag}
|
|
188
|
+
* is te be constructed.
|
|
189
|
+
* @param options - (optional) set of {@link Bcp47.ILanguageTagInitOptions | init options}
|
|
190
|
+
* to guide the validation and normalization of this tag.
|
|
191
|
+
* @returns `Success` with the new {@link Bcp47.LanguageTag | language tag} or `Failure`
|
|
192
|
+
* with details if an error occurs.
|
|
193
|
+
*/
|
|
194
|
+
static createFromTag(tag, options) {
|
|
195
|
+
options = this._getOptions(options);
|
|
196
|
+
return LanguageTagParser.parse(tag, options.iana).onSuccess((subtags) => {
|
|
197
|
+
return this._createTransformed(subtags, 'unknown', 'unknown', options);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Creates a new {@link Bcp47.LanguageTag | language tag} from a supplied
|
|
202
|
+
* {@link Bcp47.Subtags | subtags} using optional configuration,
|
|
203
|
+
* if supplied.
|
|
204
|
+
* @param tag - The {@link Bcp47.Subtags | subtags} from which the
|
|
205
|
+
* {@link Bcp47.LanguageTag | language tag} is te be constructed.
|
|
206
|
+
* @param options - (optional) set of {@link Bcp47.ILanguageTagInitOptions | init options}
|
|
207
|
+
* to guide the validation and normalization of this tag.
|
|
208
|
+
* @returns `Success` with the new {@link Bcp47.LanguageTag | language tag} or `Failure`
|
|
209
|
+
* with details if an error occurs.
|
|
210
|
+
*/
|
|
211
|
+
static createFromSubtags(subtags, options) {
|
|
212
|
+
return this._createTransformed(subtags, 'unknown', 'unknown', options);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Creates a new {@link Bcp47.LanguageTag | language tag} from a supplied `string`
|
|
216
|
+
* tag or {@link Bcp47.Subtags | subtags} using optional configuration,
|
|
217
|
+
* if supplied.
|
|
218
|
+
* @param from - The `string` tag or {@link Bcp47.Subtags | subtags} from
|
|
219
|
+
* which the {@link Bcp47.LanguageTag | language tag} is te be constructed.
|
|
220
|
+
* @param options - (optional) set of {@link Bcp47.ILanguageTagInitOptions | init options}
|
|
221
|
+
* to guide the validation and normalization of this tag.
|
|
222
|
+
* @returns `Success` with the new {@link Bcp47.LanguageTag | language tag} or `Failure`
|
|
223
|
+
* with details if an error occurs.
|
|
224
|
+
*/
|
|
225
|
+
static create(from, options) {
|
|
226
|
+
if (typeof from === 'string') {
|
|
227
|
+
return this.createFromTag(from, options);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
return this.createFromSubtags(from, options);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Constructs a new {@link Bcp47.LanguageTag | language tag} by applying appropriate transformations
|
|
235
|
+
* to as supplied {@link Bcp47.Subtags | Bcp47.Subtags}.
|
|
236
|
+
* @param subtags - The {@link Bcp47.Subtags | subtags} which represent the tag.
|
|
237
|
+
* @param fromValidity - The {@link Bcp47.TagValidity | validation level} of the supplied subtags.
|
|
238
|
+
* @param fromNormalization - The {@link Bcp47.TagNormalization | normalization level} fo the
|
|
239
|
+
* supplied subtags.
|
|
240
|
+
* @param partialOptions - Any {@link Bcp47.ILanguageTagInitOptions | initialization options}.
|
|
241
|
+
* @returns `Success` with the corresponding {@link Bcp47.LanguageTag | language tag} or `Failure`
|
|
242
|
+
* with details if an error occurs.
|
|
243
|
+
* @internal
|
|
244
|
+
*/
|
|
245
|
+
static _createTransformed(subtags, fromValidity, fromNormalization, partialOptions) {
|
|
246
|
+
const options = this._getOptions(partialOptions);
|
|
247
|
+
return ValidateTag.validateSubtags(subtags, options.validity, fromValidity)
|
|
248
|
+
.onSuccess(() => {
|
|
249
|
+
return NormalizeTag.normalizeSubtags(subtags, options.normalization, fromNormalization);
|
|
250
|
+
})
|
|
251
|
+
.onSuccess((normalized) => {
|
|
252
|
+
const validity = mostValid(fromValidity, options.validity);
|
|
253
|
+
const normalization = mostNormalized(fromNormalization, options.normalization);
|
|
254
|
+
return captureResult(() => new LanguageTag(normalized, validity, normalization, options.iana));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Gets a fully-specified {@link Bcp47.ILanguageTagInitOptions} from partial or undefined
|
|
259
|
+
* options, substituting defaults as appropriate.
|
|
260
|
+
* @param options - The {@link Bcp47.ILanguageTagInitOptions} to be expanded, or `undefined`
|
|
261
|
+
* for default options.
|
|
262
|
+
* @returns Fully-specified {@link Bcp47.ILanguageTagInitOptions | init options}.
|
|
263
|
+
* @internal
|
|
264
|
+
*/
|
|
265
|
+
static _getOptions(options) {
|
|
266
|
+
var _a, _b, _c;
|
|
267
|
+
return {
|
|
268
|
+
iana: (_a = options === null || options === void 0 ? void 0 : options.iana) !== null && _a !== void 0 ? _a : Iana.DefaultRegistries.languageRegistries,
|
|
269
|
+
validity: (_b = options === null || options === void 0 ? void 0 : options.validity) !== null && _b !== void 0 ? _b : 'well-formed',
|
|
270
|
+
normalization: (_c = options === null || options === void 0 ? void 0 : options.normalization) !== null && _c !== void 0 ? _c : 'none'
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Returns the `Suppress-Script` value defined for the primary language of this tag,
|
|
275
|
+
* regardless of whether a different script is defined in this subtag.
|
|
276
|
+
* @returns The suppress-script defined for the primary language, or undefined if
|
|
277
|
+
* the primary language is invalid or has no defined suppressed script.
|
|
278
|
+
*/
|
|
279
|
+
getSuppressedScript() {
|
|
280
|
+
if (this._suppressedScript === undefined) {
|
|
281
|
+
this._suppressedScript = false;
|
|
282
|
+
if (this.subtags.primaryLanguage) {
|
|
283
|
+
const language = this._iana.subtags.languages.tryGet(this.subtags.primaryLanguage);
|
|
284
|
+
if ((language === null || language === void 0 ? void 0 : language.suppressScript) !== undefined) {
|
|
285
|
+
this._suppressedScript = language.suppressScript;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return this._suppressedScript ? this._suppressedScript : undefined;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Gets a confirmed valid representation of this language tag.
|
|
293
|
+
* @returns `Success` with a valid representation of this {@link Bcp47.LanguageTag | language tag},
|
|
294
|
+
* or `Failure` with details if the tag cannot be validated.
|
|
295
|
+
*/
|
|
296
|
+
toValid() {
|
|
297
|
+
if (this.isValid) {
|
|
298
|
+
return succeed(this);
|
|
299
|
+
}
|
|
300
|
+
const options = {
|
|
301
|
+
iana: this._iana,
|
|
302
|
+
validity: 'valid',
|
|
303
|
+
normalization: this._normalization
|
|
304
|
+
};
|
|
305
|
+
return LanguageTag._createTransformed(this.subtags, this._validity, this._normalization, options);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Gets a confirmed strictly valid representation of this language tag.
|
|
309
|
+
* @returns `Success` with a strictly valid representation of this {@link Bcp47.LanguageTag | language tag},
|
|
310
|
+
* or `Failure` with details if the tag cannot be strictly validated.
|
|
311
|
+
*/
|
|
312
|
+
toStrictlyValid() {
|
|
313
|
+
if (this.isStrictlyValid) {
|
|
314
|
+
return succeed(this);
|
|
315
|
+
}
|
|
316
|
+
const options = {
|
|
317
|
+
iana: this._iana,
|
|
318
|
+
validity: 'strictly-valid',
|
|
319
|
+
normalization: this._normalization
|
|
320
|
+
};
|
|
321
|
+
return LanguageTag._createTransformed(this.subtags, this._validity, this._normalization, options);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Gets a confirmed canonical representation of this language tag.
|
|
325
|
+
* @returns `Success` with a canonical representation of this {@link Bcp47.LanguageTag | language tag},
|
|
326
|
+
* or `Failure` with details if the tag cannot be normalized to canonical form.
|
|
327
|
+
*/
|
|
328
|
+
toCanonical() {
|
|
329
|
+
if (this.isCanonical) {
|
|
330
|
+
return succeed(this);
|
|
331
|
+
}
|
|
332
|
+
const options = {
|
|
333
|
+
iana: this._iana,
|
|
334
|
+
validity: this._validity,
|
|
335
|
+
normalization: 'canonical'
|
|
336
|
+
};
|
|
337
|
+
return LanguageTag._createTransformed(this.subtags, this._validity, this._normalization, options);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Gets a confirmed preferred representation of this language tag.
|
|
341
|
+
* @returns `Success` with a preferred representation of this {@link Bcp47.LanguageTag | language tag},
|
|
342
|
+
* or `Failure` with details if the tag cannot be normalized to preferred form.
|
|
343
|
+
*/
|
|
344
|
+
toPreferred() {
|
|
345
|
+
if (this.isPreferred) {
|
|
346
|
+
return succeed(this);
|
|
347
|
+
}
|
|
348
|
+
const options = {
|
|
349
|
+
iana: this._iana,
|
|
350
|
+
validity: 'valid', // preferred requires validity
|
|
351
|
+
normalization: 'preferred'
|
|
352
|
+
};
|
|
353
|
+
return LanguageTag._createTransformed(this.subtags, this._validity, this._normalization, options);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Gets a string representation of this language tag.
|
|
357
|
+
* @returns A string representation of this language tag.
|
|
358
|
+
*/
|
|
359
|
+
toString() {
|
|
360
|
+
return this.tag;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=languageTag.js.map
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2022 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import * as Iana from '../iana';
|
|
23
|
+
import { allSucceed, fail, succeed } from '@fgv/ts-utils';
|
|
24
|
+
import { Validate } from './bcp47Subtags';
|
|
25
|
+
/**
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export class LanguageTagParser {
|
|
29
|
+
/* c8 ignore next */
|
|
30
|
+
constructor() { }
|
|
31
|
+
/**
|
|
32
|
+
* Parses a string representation of a BCP-47 ({@link https://www.rfc-editor.org/rfc/rfc5646.html | RFC 5646})
|
|
33
|
+
* language tag, to produce a {@link Bcp47.Subtags | Subtags} object which
|
|
34
|
+
* breaks out each of the subtags.
|
|
35
|
+
* @param tag - The `string` language tag to be parsed.
|
|
36
|
+
* @param iana - Optional {@link Iana.LanguageRegistries | IANA language registries}
|
|
37
|
+
* to be used.
|
|
38
|
+
* @returns `Success` with the resulting {@link Bcp47.Subtags | subtags}
|
|
39
|
+
* or `Failure` with details if an error occurs.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
static parse(tag, iana) {
|
|
43
|
+
/* c8 ignore next */
|
|
44
|
+
iana = iana !== null && iana !== void 0 ? iana : Iana.DefaultRegistries.languageRegistries;
|
|
45
|
+
const status = {
|
|
46
|
+
tag,
|
|
47
|
+
iana,
|
|
48
|
+
subtags: tag.split('-'),
|
|
49
|
+
parsedSubtags: {}
|
|
50
|
+
};
|
|
51
|
+
status.next = status.subtags.shift();
|
|
52
|
+
return allSucceed([
|
|
53
|
+
this._parseGrandfatheredTag(status),
|
|
54
|
+
this._parsePrimaryLanguage(status),
|
|
55
|
+
this._parseExtlang(status),
|
|
56
|
+
this._parseScript(status),
|
|
57
|
+
this._parseRegion(status),
|
|
58
|
+
this._parseVariants(status),
|
|
59
|
+
this._parseExtensions(status),
|
|
60
|
+
this._parsePrivateSubtags(status),
|
|
61
|
+
this._parseTagEnd(status)
|
|
62
|
+
], status).onSuccess(() => {
|
|
63
|
+
return succeed(status.parsedSubtags);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Determines if the entire tag matches a registered grandfathered tag.
|
|
68
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
69
|
+
* parse operation.
|
|
70
|
+
* @returns `Success` with the updated {@link Bcp47.Subtags | subtags}
|
|
71
|
+
* if a grandfathered tag is found, or `Success` with the supplied subtags
|
|
72
|
+
* if no matching grandfathered tag is found.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
static _parseGrandfatheredTag(state) {
|
|
76
|
+
const grandfathered = state.iana.subtags.grandfathered.tryGet(state.tag);
|
|
77
|
+
if (grandfathered) {
|
|
78
|
+
state.parsedSubtags.grandfathered = state.tag;
|
|
79
|
+
// we consumed the whole thing
|
|
80
|
+
state.subtags.splice(0, state.subtags.length);
|
|
81
|
+
state.next = undefined;
|
|
82
|
+
}
|
|
83
|
+
return succeed(state.parsedSubtags);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parses the primary language subtag of a supplied language tag.
|
|
87
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
88
|
+
* parse operation.
|
|
89
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
90
|
+
* updated to include the primary language subtag, or `Failure` with details if an error
|
|
91
|
+
* occurs.
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
static _parsePrimaryLanguage(state) {
|
|
95
|
+
// primary language subtag is required unless the entire tag is grandfathered or consists
|
|
96
|
+
// of only private tags
|
|
97
|
+
if (state.iana.subtags.languages.isWellFormed(state.next)) {
|
|
98
|
+
state.parsedSubtags.primaryLanguage = state.next;
|
|
99
|
+
state.next = state.subtags.shift();
|
|
100
|
+
return succeed(state.parsedSubtags);
|
|
101
|
+
}
|
|
102
|
+
else if (state.parsedSubtags.grandfathered !== undefined) {
|
|
103
|
+
return succeed(state.parsedSubtags);
|
|
104
|
+
}
|
|
105
|
+
else if (Validate.privateUsePrefix.isWellFormed(state.next)) {
|
|
106
|
+
// just return with no primary language and the private tag
|
|
107
|
+
// parser will be invoked by the parent flow.
|
|
108
|
+
return succeed(state.parsedSubtags);
|
|
109
|
+
}
|
|
110
|
+
return fail(`${state.tag}: no primary language subtag`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parses the extlang subtag(s) of a supplied language tag.
|
|
114
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
115
|
+
* parse operation.
|
|
116
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
117
|
+
* updated to include extlang subtags if present, or `Failure` with details if an error
|
|
118
|
+
* occurs.
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
static _parseExtlang(state) {
|
|
122
|
+
// optional extlangs subtags
|
|
123
|
+
while (state.iana.subtags.extlangs.isWellFormed(state.next)) {
|
|
124
|
+
if (state.parsedSubtags.extlangs === undefined) {
|
|
125
|
+
state.parsedSubtags.extlangs = [state.next];
|
|
126
|
+
}
|
|
127
|
+
else if (state.parsedSubtags.extlangs.length < 3) {
|
|
128
|
+
state.parsedSubtags.extlangs.push(state.next);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
return fail(`${state.next}: too many extlang subtags`);
|
|
132
|
+
}
|
|
133
|
+
state.next = state.subtags.shift();
|
|
134
|
+
}
|
|
135
|
+
return succeed(state.parsedSubtags);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Parses the script subtag of a supplied language tag.
|
|
139
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
140
|
+
* parse operation.
|
|
141
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
142
|
+
* updated to include the script subtag if present, or `Failure` with details if an error
|
|
143
|
+
* occurs.
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
static _parseScript(state) {
|
|
147
|
+
// optional script subtag
|
|
148
|
+
if (state.iana.subtags.scripts.isWellFormed(state.next)) {
|
|
149
|
+
state.parsedSubtags.script = state.next;
|
|
150
|
+
state.next = state.subtags.shift();
|
|
151
|
+
}
|
|
152
|
+
return succeed(state.parsedSubtags);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Parses the region subtag of a supplied language tag.
|
|
156
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
157
|
+
* parse operation.
|
|
158
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
159
|
+
* updated to include the region subtag if present, or `Failure` with details if an error
|
|
160
|
+
* occurs.
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
static _parseRegion(state) {
|
|
164
|
+
// optional region subtag
|
|
165
|
+
if (state.iana.subtags.regions.isWellFormed(state.next)) {
|
|
166
|
+
state.parsedSubtags.region = state.next;
|
|
167
|
+
state.next = state.subtags.shift();
|
|
168
|
+
}
|
|
169
|
+
return succeed(state.parsedSubtags);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parses the variant subtag(s) of a supplied language tag.
|
|
173
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
174
|
+
* parse operation.
|
|
175
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
176
|
+
* updated to include the variant subtags if present, or `Failure` with details if an error
|
|
177
|
+
* occurs.
|
|
178
|
+
* @internal
|
|
179
|
+
*/
|
|
180
|
+
static _parseVariants(state) {
|
|
181
|
+
// optional variant subtags
|
|
182
|
+
while (state.iana.subtags.variants.isWellFormed(state.next)) {
|
|
183
|
+
if (state.parsedSubtags.variants === undefined) {
|
|
184
|
+
state.parsedSubtags.variants = [state.next];
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
state.parsedSubtags.variants.push(state.next);
|
|
188
|
+
}
|
|
189
|
+
state.next = state.subtags.shift();
|
|
190
|
+
}
|
|
191
|
+
return succeed(state.parsedSubtags);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parses the extension subtag(s) of a supplied language tag.
|
|
195
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
196
|
+
* parse operation.
|
|
197
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
198
|
+
* updated to include the extension subtags if present, or `Failure` with details if an error
|
|
199
|
+
* occurs.
|
|
200
|
+
* @internal
|
|
201
|
+
*/
|
|
202
|
+
static _parseExtensions(state) {
|
|
203
|
+
// optional extension subtags
|
|
204
|
+
while (state.next !== undefined && Validate.extensionSingleton.isWellFormed(state.next)) {
|
|
205
|
+
const singleton = state.next;
|
|
206
|
+
const values = [];
|
|
207
|
+
state.next = state.subtags.shift();
|
|
208
|
+
while (Validate.extensionSubtag.isWellFormed(state.next)) {
|
|
209
|
+
values.push(state.next);
|
|
210
|
+
state.next = state.subtags.shift();
|
|
211
|
+
}
|
|
212
|
+
if (state.next !== undefined &&
|
|
213
|
+
!Validate.extensionSingleton.isWellFormed(state.next) &&
|
|
214
|
+
!Validate.privateUsePrefix.isWellFormed(state.next)) {
|
|
215
|
+
return fail(`${state.next}: malformed extension subtag`);
|
|
216
|
+
}
|
|
217
|
+
else if (values.length < 1) {
|
|
218
|
+
return fail(`${state.tag}: extension '${singleton}' must have at least one subtag.`);
|
|
219
|
+
}
|
|
220
|
+
const value = values.join('-');
|
|
221
|
+
if (state.parsedSubtags.extensions === undefined) {
|
|
222
|
+
state.parsedSubtags.extensions = [{ singleton, value }];
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
state.parsedSubtags.extensions.push({ singleton, value });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return succeed(state.parsedSubtags);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Parses the private use subtags of a supplied language tag.
|
|
232
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
233
|
+
* parse operation.
|
|
234
|
+
* @returns `Success` with supplied {@link Bcp47.Subtags | subtags}
|
|
235
|
+
* updated to include the private-use subtags if present, or `Failure` with details if an error
|
|
236
|
+
* occurs.
|
|
237
|
+
* @internal
|
|
238
|
+
*/
|
|
239
|
+
static _parsePrivateSubtags(state) {
|
|
240
|
+
// optional private use subtags
|
|
241
|
+
if (state.next !== undefined && Validate.privateUsePrefix.isWellFormed(state.next)) {
|
|
242
|
+
const values = [];
|
|
243
|
+
state.next = state.subtags.shift();
|
|
244
|
+
while (state.next &&
|
|
245
|
+
state.next.length > 1 &&
|
|
246
|
+
Iana.LanguageSubtags.Validate.extendedLanguageRange.isWellFormed(state.next)) {
|
|
247
|
+
values.push(state.next);
|
|
248
|
+
state.next = state.subtags.shift();
|
|
249
|
+
}
|
|
250
|
+
if (state.next !== undefined) {
|
|
251
|
+
return fail(`${state.next}: malformed private-use subtag`);
|
|
252
|
+
}
|
|
253
|
+
else if (values.length < 1) {
|
|
254
|
+
return fail(`${state.tag}: private-use tag must have at least one subtag.`);
|
|
255
|
+
}
|
|
256
|
+
state.parsedSubtags.privateUse = values;
|
|
257
|
+
}
|
|
258
|
+
return succeed(state.parsedSubtags);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Verifies {@link Bcp47.ParserState | parser state} at the end of a parse operation.
|
|
262
|
+
* @param state - The {@link Bcp47.ParserState | current state} of the
|
|
263
|
+
* parse operation.
|
|
264
|
+
* @returns `Success` if the tag was fully consumed, or `Failure` with details
|
|
265
|
+
* if unexpected subtags remain to be parsed.
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
268
|
+
static _parseTagEnd(state) {
|
|
269
|
+
if (state.next !== undefined) {
|
|
270
|
+
return fail(`${state.next}: unexpected subtag`);
|
|
271
|
+
}
|
|
272
|
+
return succeed(state.parsedSubtags);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=languageTagParser.js.map
|