/src/serenity/Userland/Libraries/LibLocale/Locale.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/AllOf.h> |
8 | | #include <AK/GenericLexer.h> |
9 | | #include <AK/QuickSort.h> |
10 | | #include <AK/StringBuilder.h> |
11 | | #include <LibLocale/DateTimeFormat.h> |
12 | | #include <LibLocale/Locale.h> |
13 | | #include <LibUnicode/CharacterTypes.h> |
14 | | |
15 | | namespace Locale { |
16 | | |
17 | | static bool is_key(StringView key) |
18 | 0 | { |
19 | | // key = alphanum alpha |
20 | 0 | if (key.length() != 2) |
21 | 0 | return false; |
22 | 0 | return is_ascii_alphanumeric(key[0]) && is_ascii_alpha(key[1]); |
23 | 0 | } |
24 | | |
25 | | static bool is_single_type(StringView type) |
26 | 0 | { |
27 | | // type = alphanum{3,8} (sep alphanum{3,8})* |
28 | | // Note: Consecutive types are not handled here, that is left to the caller. |
29 | 0 | if ((type.length() < 3) || (type.length() > 8)) |
30 | 0 | return false; |
31 | 0 | return all_of(type, is_ascii_alphanumeric); |
32 | 0 | } |
33 | | |
34 | | static bool is_attribute(StringView type) |
35 | 0 | { |
36 | | // attribute = alphanum{3,8} |
37 | 0 | if ((type.length() < 3) || (type.length() > 8)) |
38 | 0 | return false; |
39 | 0 | return all_of(type, is_ascii_alphanumeric); |
40 | 0 | } |
41 | | |
42 | | static bool is_transformed_key(StringView key) |
43 | 0 | { |
44 | | // tkey = alpha digit |
45 | 0 | if (key.length() != 2) |
46 | 0 | return false; |
47 | 0 | return is_ascii_alpha(key[0]) && is_ascii_digit(key[1]); |
48 | 0 | } |
49 | | |
50 | | static bool is_single_transformed_value(StringView value) |
51 | 0 | { |
52 | | // tvalue = (sep alphanum{3,8})+ |
53 | | // Note: Consecutive values are not handled here, that is left to the caller. |
54 | 0 | if ((value.length() < 3) || (value.length() > 8)) |
55 | 0 | return false; |
56 | 0 | return all_of(value, is_ascii_alphanumeric); |
57 | 0 | } |
58 | | |
59 | | static Optional<StringView> consume_next_segment(GenericLexer& lexer, bool with_separator = true) |
60 | 0 | { |
61 | 0 | constexpr auto is_separator = is_any_of("-_"sv); |
62 | |
|
63 | 0 | if (with_separator) { |
64 | 0 | if (!lexer.next_is(is_separator)) |
65 | 0 | return {}; |
66 | 0 | lexer.ignore(); |
67 | 0 | } |
68 | | |
69 | 0 | auto segment = lexer.consume_until(is_separator); |
70 | 0 | if (segment.is_empty()) { |
71 | 0 | lexer.retreat(with_separator); |
72 | 0 | return {}; |
73 | 0 | } |
74 | | |
75 | 0 | return segment; |
76 | 0 | } |
77 | | |
78 | | bool is_type_identifier(StringView identifier) |
79 | 0 | { |
80 | | // type = alphanum{3,8} (sep alphanum{3,8})* |
81 | 0 | GenericLexer lexer { identifier }; |
82 | |
|
83 | 0 | while (true) { |
84 | 0 | auto type = consume_next_segment(lexer, lexer.tell() > 0); |
85 | 0 | if (!type.has_value()) |
86 | 0 | break; |
87 | 0 | if (!is_single_type(*type)) |
88 | 0 | return false; |
89 | 0 | } |
90 | | |
91 | 0 | return lexer.is_eof() && (lexer.tell() > 0); |
92 | 0 | } |
93 | | |
94 | | static Optional<LanguageID> parse_unicode_language_id(GenericLexer& lexer) |
95 | 0 | { |
96 | | // https://unicode.org/reports/tr35/#Unicode_language_identifier |
97 | | // |
98 | | // unicode_language_id = "root" |
99 | | // OR |
100 | | // unicode_language_id = ((unicode_language_subtag (sep unicode_script_subtag)?) | unicode_script_subtag) |
101 | | // (sep unicode_region_subtag)? |
102 | | // (sep unicode_variant_subtag)* |
103 | 0 | LanguageID language_id {}; |
104 | |
|
105 | 0 | if (lexer.consume_specific("root"sv)) { |
106 | 0 | language_id.is_root = true; |
107 | 0 | return language_id; |
108 | 0 | } |
109 | | |
110 | 0 | enum class ParseState { |
111 | 0 | ParsingLanguageOrScript, |
112 | 0 | ParsingScript, |
113 | 0 | ParsingRegion, |
114 | 0 | ParsingVariant, |
115 | 0 | Done, |
116 | 0 | }; |
117 | |
|
118 | 0 | auto state = ParseState::ParsingLanguageOrScript; |
119 | |
|
120 | 0 | while (!lexer.is_eof() && (state != ParseState::Done)) { |
121 | 0 | auto segment = consume_next_segment(lexer, state != ParseState::ParsingLanguageOrScript); |
122 | 0 | if (!segment.has_value()) |
123 | 0 | return {}; |
124 | | |
125 | 0 | switch (state) { |
126 | 0 | case ParseState::ParsingLanguageOrScript: |
127 | 0 | if (is_unicode_language_subtag(*segment)) { |
128 | 0 | state = ParseState::ParsingScript; |
129 | 0 | language_id.language = MUST(String::from_utf8(*segment)); |
130 | 0 | } else if (is_unicode_script_subtag(*segment)) { |
131 | 0 | state = ParseState::ParsingRegion; |
132 | 0 | language_id.script = MUST(String::from_utf8(*segment)); |
133 | 0 | } else { |
134 | 0 | return {}; |
135 | 0 | } |
136 | 0 | break; |
137 | | |
138 | 0 | case ParseState::ParsingScript: |
139 | 0 | if (is_unicode_script_subtag(*segment)) { |
140 | 0 | state = ParseState::ParsingRegion; |
141 | 0 | language_id.script = MUST(String::from_utf8(*segment)); |
142 | 0 | break; |
143 | 0 | } |
144 | | |
145 | 0 | state = ParseState::ParsingRegion; |
146 | 0 | [[fallthrough]]; |
147 | |
|
148 | 0 | case ParseState::ParsingRegion: |
149 | 0 | if (is_unicode_region_subtag(*segment)) { |
150 | 0 | state = ParseState::ParsingVariant; |
151 | 0 | language_id.region = MUST(String::from_utf8(*segment)); |
152 | 0 | break; |
153 | 0 | } |
154 | | |
155 | 0 | state = ParseState::ParsingVariant; |
156 | 0 | [[fallthrough]]; |
157 | |
|
158 | 0 | case ParseState::ParsingVariant: |
159 | 0 | if (is_unicode_variant_subtag(*segment)) { |
160 | 0 | language_id.variants.append(MUST(String::from_utf8(*segment))); |
161 | 0 | } else { |
162 | 0 | lexer.retreat(segment->length() + 1); |
163 | 0 | state = ParseState::Done; |
164 | 0 | } |
165 | 0 | break; |
166 | | |
167 | 0 | default: |
168 | 0 | VERIFY_NOT_REACHED(); |
169 | 0 | } |
170 | 0 | } |
171 | | |
172 | 0 | return language_id; |
173 | 0 | } |
174 | | |
175 | | static Optional<LocaleExtension> parse_unicode_locale_extension(GenericLexer& lexer) |
176 | 0 | { |
177 | | // https://unicode.org/reports/tr35/#unicode_locale_extensions |
178 | | // |
179 | | // unicode_locale_extensions = sep [uU] ((sep keyword)+ | (sep attribute)+ (sep keyword)*) |
180 | 0 | LocaleExtension locale_extension {}; |
181 | |
|
182 | 0 | enum class ParseState { |
183 | 0 | ParsingAttributeOrKeyword, |
184 | 0 | ParsingAttribute, |
185 | 0 | ParsingKeyword, |
186 | 0 | Done, |
187 | 0 | }; |
188 | |
|
189 | 0 | auto state = ParseState::ParsingAttributeOrKeyword; |
190 | |
|
191 | 0 | while (!lexer.is_eof() && (state != ParseState::Done)) { |
192 | 0 | auto segment = consume_next_segment(lexer); |
193 | 0 | if (!segment.has_value()) |
194 | 0 | return {}; |
195 | | |
196 | 0 | if (state == ParseState::ParsingAttributeOrKeyword) |
197 | 0 | state = is_key(*segment) ? ParseState::ParsingKeyword : ParseState::ParsingAttribute; |
198 | |
|
199 | 0 | switch (state) { |
200 | 0 | case ParseState::ParsingAttribute: |
201 | 0 | if (is_attribute(*segment)) { |
202 | 0 | locale_extension.attributes.append(MUST(String::from_utf8(*segment))); |
203 | 0 | break; |
204 | 0 | } |
205 | | |
206 | 0 | state = ParseState::ParsingKeyword; |
207 | 0 | [[fallthrough]]; |
208 | |
|
209 | 0 | case ParseState::ParsingKeyword: { |
210 | | // keyword = key (sep type)? |
211 | 0 | Keyword keyword { .key = MUST(String::from_utf8(*segment)) }; |
212 | 0 | Vector<StringView> keyword_values; |
213 | |
|
214 | 0 | if (!is_key(*segment)) { |
215 | 0 | lexer.retreat(segment->length() + 1); |
216 | 0 | state = ParseState::Done; |
217 | 0 | break; |
218 | 0 | } |
219 | | |
220 | 0 | while (true) { |
221 | 0 | auto type = consume_next_segment(lexer); |
222 | |
|
223 | 0 | if (!type.has_value() || !is_single_type(*type)) { |
224 | 0 | if (type.has_value()) |
225 | 0 | lexer.retreat(type->length() + 1); |
226 | 0 | break; |
227 | 0 | } |
228 | | |
229 | 0 | keyword_values.append(*type); |
230 | 0 | } |
231 | |
|
232 | 0 | StringBuilder builder; |
233 | 0 | builder.join('-', keyword_values); |
234 | 0 | keyword.value = MUST(builder.to_string()); |
235 | |
|
236 | 0 | locale_extension.keywords.append(move(keyword)); |
237 | 0 | break; |
238 | 0 | } |
239 | | |
240 | 0 | default: |
241 | 0 | VERIFY_NOT_REACHED(); |
242 | 0 | } |
243 | 0 | } |
244 | | |
245 | 0 | if (locale_extension.attributes.is_empty() && locale_extension.keywords.is_empty()) |
246 | 0 | return {}; |
247 | 0 | return locale_extension; |
248 | 0 | } |
249 | | |
250 | | static Optional<TransformedExtension> parse_transformed_extension(GenericLexer& lexer) |
251 | 0 | { |
252 | | // https://unicode.org/reports/tr35/#transformed_extensions |
253 | | // |
254 | | // transformed_extensions = sep [tT] ((sep tlang (sep tfield)*) | (sep tfield)+) |
255 | 0 | TransformedExtension transformed_extension {}; |
256 | |
|
257 | 0 | enum class ParseState { |
258 | 0 | ParsingLanguageOrField, |
259 | 0 | ParsingLanguage, |
260 | 0 | ParsingField, |
261 | 0 | Done, |
262 | 0 | }; |
263 | |
|
264 | 0 | auto state = ParseState::ParsingLanguageOrField; |
265 | |
|
266 | 0 | while (!lexer.is_eof() && (state != ParseState::Done)) { |
267 | 0 | auto segment = consume_next_segment(lexer); |
268 | 0 | if (!segment.has_value()) |
269 | 0 | return {}; |
270 | | |
271 | 0 | if (state == ParseState::ParsingLanguageOrField) |
272 | 0 | state = is_unicode_language_subtag(*segment) ? ParseState::ParsingLanguage : ParseState::ParsingField; |
273 | |
|
274 | 0 | switch (state) { |
275 | 0 | case ParseState::ParsingLanguage: |
276 | 0 | lexer.retreat(segment->length()); |
277 | |
|
278 | 0 | if (auto language_id = parse_unicode_language_id(lexer); language_id.has_value()) { |
279 | 0 | transformed_extension.language = language_id.release_value(); |
280 | 0 | state = ParseState::ParsingField; |
281 | 0 | break; |
282 | 0 | } |
283 | | |
284 | 0 | return {}; |
285 | | |
286 | 0 | case ParseState::ParsingField: { |
287 | | // tfield = tkey tvalue; |
288 | 0 | TransformedField field { .key = MUST(String::from_utf8(*segment)) }; |
289 | 0 | Vector<StringView> field_values; |
290 | |
|
291 | 0 | if (!is_transformed_key(*segment)) { |
292 | 0 | lexer.retreat(segment->length() + 1); |
293 | 0 | state = ParseState::Done; |
294 | 0 | break; |
295 | 0 | } |
296 | | |
297 | 0 | while (true) { |
298 | 0 | auto value = consume_next_segment(lexer); |
299 | |
|
300 | 0 | if (!value.has_value() || !is_single_transformed_value(*value)) { |
301 | 0 | if (value.has_value()) |
302 | 0 | lexer.retreat(value->length() + 1); |
303 | 0 | break; |
304 | 0 | } |
305 | | |
306 | 0 | field_values.append(*value); |
307 | 0 | } |
308 | |
|
309 | 0 | if (field_values.is_empty()) |
310 | 0 | return {}; |
311 | | |
312 | 0 | StringBuilder builder; |
313 | 0 | builder.join('-', field_values); |
314 | 0 | field.value = MUST(builder.to_string()); |
315 | |
|
316 | 0 | transformed_extension.fields.append(move(field)); |
317 | 0 | break; |
318 | 0 | } |
319 | | |
320 | 0 | default: |
321 | 0 | VERIFY_NOT_REACHED(); |
322 | 0 | } |
323 | 0 | } |
324 | | |
325 | 0 | if (!transformed_extension.language.has_value() && transformed_extension.fields.is_empty()) |
326 | 0 | return {}; |
327 | 0 | return transformed_extension; |
328 | 0 | } |
329 | | |
330 | | static Optional<OtherExtension> parse_other_extension(char key, GenericLexer& lexer) |
331 | 0 | { |
332 | | // https://unicode.org/reports/tr35/#other_extensions |
333 | | // |
334 | | // other_extensions = sep [alphanum-[tTuUxX]] (sep alphanum{2,8})+ ; |
335 | 0 | OtherExtension other_extension { .key = key }; |
336 | 0 | Vector<StringView> other_values; |
337 | |
|
338 | 0 | if (!is_ascii_alphanumeric(key) || (key == 'x') || (key == 'X')) |
339 | 0 | return {}; |
340 | | |
341 | 0 | while (true) { |
342 | 0 | auto segment = consume_next_segment(lexer); |
343 | 0 | if (!segment.has_value()) |
344 | 0 | break; |
345 | | |
346 | 0 | if ((segment->length() < 2) || (segment->length() > 8) || !all_of(*segment, is_ascii_alphanumeric)) { |
347 | 0 | lexer.retreat(segment->length() + 1); |
348 | 0 | break; |
349 | 0 | } |
350 | | |
351 | 0 | other_values.append(*segment); |
352 | 0 | } |
353 | |
|
354 | 0 | if (other_values.is_empty()) |
355 | 0 | return {}; |
356 | | |
357 | 0 | StringBuilder builder; |
358 | 0 | builder.join('-', other_values); |
359 | 0 | other_extension.value = MUST(builder.to_string()); |
360 | |
|
361 | 0 | return other_extension; |
362 | 0 | } |
363 | | |
364 | | static Optional<Extension> parse_extension(GenericLexer& lexer) |
365 | 0 | { |
366 | | // https://unicode.org/reports/tr35/#extensions |
367 | | // |
368 | | // extensions = unicode_locale_extensions | transformed_extensions | other_extensions |
369 | 0 | size_t starting_position = lexer.tell(); |
370 | |
|
371 | 0 | if (auto header = consume_next_segment(lexer); header.has_value() && (header->length() == 1)) { |
372 | 0 | switch (char key = (*header)[0]) { |
373 | 0 | case 'u': |
374 | 0 | case 'U': |
375 | 0 | if (auto extension = parse_unicode_locale_extension(lexer); extension.has_value()) |
376 | 0 | return Extension { extension.release_value() }; |
377 | 0 | break; |
378 | | |
379 | 0 | case 't': |
380 | 0 | case 'T': |
381 | 0 | if (auto extension = parse_transformed_extension(lexer); extension.has_value()) |
382 | 0 | return Extension { extension.release_value() }; |
383 | 0 | break; |
384 | | |
385 | 0 | default: |
386 | 0 | if (auto extension = parse_other_extension(key, lexer); extension.has_value()) |
387 | 0 | return Extension { extension.release_value() }; |
388 | 0 | break; |
389 | 0 | } |
390 | 0 | } |
391 | | |
392 | 0 | lexer.retreat(lexer.tell() - starting_position); |
393 | 0 | return {}; |
394 | 0 | } |
395 | | |
396 | | static Vector<String> parse_private_use_extensions(GenericLexer& lexer) |
397 | 0 | { |
398 | | // https://unicode.org/reports/tr35/#pu_extensions |
399 | | // |
400 | | // pu_extensions = = sep [xX] (sep alphanum{1,8})+ ; |
401 | 0 | size_t starting_position = lexer.tell(); |
402 | |
|
403 | 0 | auto header = consume_next_segment(lexer); |
404 | 0 | if (!header.has_value()) |
405 | 0 | return {}; |
406 | | |
407 | 0 | auto parse_values = [&]() { |
408 | 0 | Vector<String> extensions; |
409 | |
|
410 | 0 | while (true) { |
411 | 0 | auto segment = consume_next_segment(lexer); |
412 | 0 | if (!segment.has_value()) |
413 | 0 | break; |
414 | | |
415 | 0 | if ((segment->length() < 1) || (segment->length() > 8) || !all_of(*segment, is_ascii_alphanumeric)) { |
416 | 0 | lexer.retreat(segment->length() + 1); |
417 | 0 | break; |
418 | 0 | } |
419 | | |
420 | 0 | extensions.append(MUST(String::from_utf8(*segment))); |
421 | 0 | } |
422 | | |
423 | 0 | return extensions; |
424 | 0 | }; |
425 | |
|
426 | 0 | if ((header->length() == 1) && (((*header)[0] == 'x') || ((*header)[0] == 'X'))) { |
427 | 0 | if (auto extensions = parse_values(); !extensions.is_empty()) |
428 | 0 | return extensions; |
429 | 0 | } |
430 | | |
431 | 0 | lexer.retreat(lexer.tell() - starting_position); |
432 | 0 | return {}; |
433 | 0 | } |
434 | | |
435 | | Optional<LanguageID> parse_unicode_language_id(StringView language) |
436 | 0 | { |
437 | 0 | GenericLexer lexer { language }; |
438 | |
|
439 | 0 | auto language_id = parse_unicode_language_id(lexer); |
440 | 0 | if (!lexer.is_eof()) |
441 | 0 | return {}; |
442 | | |
443 | 0 | return language_id; |
444 | 0 | } |
445 | | |
446 | | Optional<LocaleID> parse_unicode_locale_id(StringView locale) |
447 | 0 | { |
448 | 0 | GenericLexer lexer { locale }; |
449 | | |
450 | | // https://unicode.org/reports/tr35/#Unicode_locale_identifier |
451 | | // |
452 | | // unicode_locale_id = unicode_language_id |
453 | | // extensions* |
454 | | // pu_extensions? |
455 | 0 | auto language_id = parse_unicode_language_id(lexer); |
456 | 0 | if (!language_id.has_value()) |
457 | 0 | return {}; |
458 | | |
459 | 0 | LocaleID locale_id { language_id.release_value() }; |
460 | |
|
461 | 0 | while (true) { |
462 | 0 | auto extension = parse_extension(lexer); |
463 | 0 | if (!extension.has_value()) |
464 | 0 | break; |
465 | 0 | locale_id.extensions.append(extension.release_value()); |
466 | 0 | } |
467 | |
|
468 | 0 | locale_id.private_use_extensions = parse_private_use_extensions(lexer); |
469 | |
|
470 | 0 | if (!lexer.is_eof()) |
471 | 0 | return {}; |
472 | | |
473 | 0 | return locale_id; |
474 | 0 | } |
475 | | |
476 | | static void perform_hard_coded_key_value_substitutions(StringView key, String& value) |
477 | 0 | { |
478 | | // FIXME: In the XML export of CLDR, there are some aliases defined in the following files: |
479 | | // https://github.com/unicode-org/cldr-staging/blob/master/production/common/bcp47/calendar.xml |
480 | | // https://github.com/unicode-org/cldr-staging/blob/master/production/common/bcp47/collation.xml |
481 | | // https://github.com/unicode-org/cldr-staging/blob/master/production/common/bcp47/measure.xml |
482 | | // https://github.com/unicode-org/cldr-staging/blob/master/production/common/bcp47/timezone.xml |
483 | | // https://github.com/unicode-org/cldr-staging/blob/master/production/common/bcp47/transform.xml |
484 | | // |
485 | | // There isn't yet a counterpart in the JSON export. See: https://unicode-org.atlassian.net/browse/CLDR-14571 |
486 | 0 | Optional<StringView> result; |
487 | |
|
488 | 0 | if (key == "ca"sv) { |
489 | 0 | if (value == "islamicc"sv) |
490 | 0 | result = "islamic-civil"sv; |
491 | 0 | else if (value == "ethiopic-amete-alem"sv) |
492 | 0 | result = "ethioaa"sv; |
493 | 0 | } else if (key.is_one_of("kb"sv, "kc"sv, "kh"sv, "kk"sv, "kn"sv) && (value == "yes"sv)) { |
494 | 0 | result = "true"sv; |
495 | 0 | } else if (key == "ks"sv) { |
496 | 0 | if (value == "primary"sv) |
497 | 0 | result = "level1"sv; |
498 | 0 | else if (value == "tertiary"sv) |
499 | 0 | result = "level3"sv; |
500 | | // Note: There are also aliases for "secondary", "quaternary", "quarternary", and "identical", |
501 | | // but those are semantically incorrect values (they are too long), so they can be skipped. |
502 | 0 | } else if ((key == "m0"sv) && (value == "names"sv)) { |
503 | 0 | result = "prprname"sv; |
504 | 0 | } else if ((key == "ms"sv) && (value == "imperial"sv)) { |
505 | 0 | result = "uksystem"sv; |
506 | 0 | } else if (key == "tz"sv) { |
507 | | // Formatter disabled because this block is easier to read / check against timezone.xml as one-liners. |
508 | | // clang-format off |
509 | 0 | if (value == "aqams"sv) result = "nzakl"sv; |
510 | 0 | else if (value == "cnckg"sv) result = "cnsha"sv; |
511 | 0 | else if (value == "cnhrb"sv) result = "cnsha"sv; |
512 | 0 | else if (value == "cnkhg"sv) result = "cnurc"sv; |
513 | 0 | else if (value == "cuba"sv) result = "cuhav"sv; |
514 | 0 | else if (value == "egypt"sv) result = "egcai"sv; |
515 | 0 | else if (value == "eire"sv) result = "iedub"sv; |
516 | 0 | else if (value == "est"sv) result = "utcw05"sv; |
517 | 0 | else if (value == "gmt0"sv) result = "gmt"sv; |
518 | 0 | else if (value == "hongkong"sv) result = "hkhkg"sv; |
519 | 0 | else if (value == "hst"sv) result = "utcw10"sv; |
520 | 0 | else if (value == "iceland"sv) result = "isrey"sv; |
521 | 0 | else if (value == "iran"sv) result = "irthr"sv; |
522 | 0 | else if (value == "israel"sv) result = "jeruslm"sv; |
523 | 0 | else if (value == "jamaica"sv) result = "jmkin"sv; |
524 | 0 | else if (value == "japan"sv) result = "jptyo"sv; |
525 | 0 | else if (value == "kwajalein"sv) result = "mhkwa"sv; |
526 | 0 | else if (value == "libya"sv) result = "lytip"sv; |
527 | 0 | else if (value == "mst"sv) result = "utcw07"sv; |
528 | 0 | else if (value == "navajo"sv) result = "usden"sv; |
529 | 0 | else if (value == "poland"sv) result = "plwaw"sv; |
530 | 0 | else if (value == "portugal"sv) result = "ptlis"sv; |
531 | 0 | else if (value == "prc"sv) result = "cnsha"sv; |
532 | 0 | else if (value == "roc"sv) result = "twtpe"sv; |
533 | 0 | else if (value == "rok"sv) result = "krsel"sv; |
534 | 0 | else if (value == "singapore"sv) result = "sgsin"sv; |
535 | 0 | else if (value == "turkey"sv) result = "trist"sv; |
536 | 0 | else if (value == "uct"sv) result = "utc"sv; |
537 | 0 | else if (value == "usnavajo"sv) result = "usden"sv; |
538 | 0 | else if (value == "zulu"sv) result = "utc"sv; |
539 | | // clang-format on |
540 | 0 | } |
541 | |
|
542 | 0 | if (result.has_value()) |
543 | 0 | value = MUST(String::from_utf8(*result)); |
544 | 0 | } |
545 | | |
546 | | void canonicalize_unicode_extension_values(StringView key, String& value, bool remove_true) |
547 | 0 | { |
548 | 0 | value = MUST(value.to_lowercase()); |
549 | 0 | perform_hard_coded_key_value_substitutions(key, value); |
550 | | |
551 | | // Note: The spec says to remove "true" type and tfield values but that is believed to be a bug in the spec |
552 | | // because, for tvalues, that would result in invalid syntax: |
553 | | // https://unicode-org.atlassian.net/browse/CLDR-14318 |
554 | | // This has also been noted by test262: |
555 | | // https://github.com/tc39/test262/blob/18bb955771669541c56c28748603f6afdb2e25ff/test/intl402/Intl/getCanonicalLocales/transformed-ext-canonical.js |
556 | 0 | if (remove_true && (value == "true"sv)) { |
557 | 0 | value = {}; |
558 | 0 | return; |
559 | 0 | } |
560 | | |
561 | 0 | if (key.is_one_of("sd"sv, "rg"sv)) { |
562 | 0 | if (auto alias = resolve_subdivision_alias(value); alias.has_value()) { |
563 | 0 | auto aliases = alias->split_view(' '); |
564 | | |
565 | | // FIXME: Subdivision subtags do not appear in the CLDR likelySubtags.json file. |
566 | | // Implement the spec's recommendation of using just the first alias for now, |
567 | | // but we should determine if there's anything else needed here. |
568 | 0 | value = MUST(String::from_utf8(aliases[0])); |
569 | 0 | } |
570 | 0 | } |
571 | 0 | } |
572 | | |
573 | | static void transform_unicode_locale_id_to_canonical_syntax(LocaleID& locale_id) |
574 | 0 | { |
575 | 0 | auto canonicalize_language = [&](LanguageID& language_id, bool force_lowercase) { |
576 | 0 | language_id.language = MUST(language_id.language->to_lowercase()); |
577 | 0 | if (language_id.script.has_value()) |
578 | 0 | language_id.script = MUST(language_id.script->to_titlecase()); |
579 | 0 | if (language_id.region.has_value()) |
580 | 0 | language_id.region = MUST(language_id.region->to_uppercase()); |
581 | 0 | for (auto& variant : language_id.variants) |
582 | 0 | variant = MUST(variant.to_lowercase()); |
583 | |
|
584 | 0 | resolve_complex_language_aliases(language_id); |
585 | |
|
586 | 0 | if (auto alias = resolve_language_alias(*language_id.language); alias.has_value()) { |
587 | 0 | auto language_alias = parse_unicode_language_id(*alias); |
588 | 0 | VERIFY(language_alias.has_value()); |
589 | | |
590 | 0 | language_id.language = move(language_alias->language); |
591 | 0 | if (!language_id.script.has_value() && language_alias->script.has_value()) |
592 | 0 | language_id.script = move(language_alias->script); |
593 | 0 | if (!language_id.region.has_value() && language_alias->region.has_value()) |
594 | 0 | language_id.region = move(language_alias->region); |
595 | 0 | if (language_id.variants.is_empty() && !language_alias->variants.is_empty()) |
596 | 0 | language_id.variants = move(language_alias->variants); |
597 | 0 | } |
598 | | |
599 | 0 | if (language_id.script.has_value()) { |
600 | 0 | if (auto alias = resolve_script_tag_alias(*language_id.script); alias.has_value()) |
601 | 0 | language_id.script = MUST(String::from_utf8(*alias)); |
602 | 0 | } |
603 | |
|
604 | 0 | if (language_id.region.has_value()) { |
605 | 0 | if (auto alias = resolve_territory_alias(*language_id.region); alias.has_value()) |
606 | 0 | language_id.region = resolve_most_likely_territory_alias(language_id, *alias); |
607 | 0 | } |
608 | |
|
609 | 0 | quick_sort(language_id.variants); |
610 | |
|
611 | 0 | for (auto& variant : language_id.variants) { |
612 | 0 | variant = MUST(variant.to_lowercase()); |
613 | 0 | if (auto alias = resolve_variant_alias(variant); alias.has_value()) |
614 | 0 | variant = MUST(String::from_utf8(*alias)); |
615 | 0 | } |
616 | |
|
617 | 0 | if (force_lowercase) { |
618 | 0 | if (language_id.script.has_value()) |
619 | 0 | language_id.script = MUST(language_id.script->to_lowercase()); |
620 | 0 | if (language_id.region.has_value()) |
621 | 0 | language_id.region = MUST(language_id.region->to_lowercase()); |
622 | 0 | } |
623 | 0 | }; |
624 | |
|
625 | 0 | canonicalize_language(locale_id.language_id, false); |
626 | |
|
627 | 0 | quick_sort(locale_id.extensions, [](auto const& left, auto const& right) { |
628 | 0 | auto key = [](auto const& extension) { |
629 | 0 | return extension.visit( |
630 | 0 | [](LocaleExtension const&) { return 'u'; }, |
631 | 0 | [](TransformedExtension const&) { return 't'; }, |
632 | 0 | [](OtherExtension const& ext) { return static_cast<char>(to_ascii_lowercase(ext.key)); }); |
633 | 0 | }; |
634 | |
|
635 | 0 | return key(left) < key(right); |
636 | 0 | }); |
637 | |
|
638 | 0 | for (auto& extension : locale_id.extensions) { |
639 | 0 | extension.visit( |
640 | 0 | [&](LocaleExtension& ext) { |
641 | 0 | for (auto& attribute : ext.attributes) |
642 | 0 | attribute = MUST(attribute.to_lowercase()); |
643 | |
|
644 | 0 | for (auto& keyword : ext.keywords) { |
645 | 0 | keyword.key = MUST(keyword.key.to_lowercase()); |
646 | 0 | canonicalize_unicode_extension_values(keyword.key, keyword.value, true); |
647 | 0 | } |
648 | |
|
649 | 0 | quick_sort(ext.attributes); |
650 | 0 | quick_sort(ext.keywords, [](auto const& a, auto const& b) { return a.key < b.key; }); |
651 | 0 | }, |
652 | 0 | [&](TransformedExtension& ext) { |
653 | 0 | if (ext.language.has_value()) |
654 | 0 | canonicalize_language(*ext.language, true); |
655 | |
|
656 | 0 | for (auto& field : ext.fields) { |
657 | 0 | field.key = MUST(field.key.to_lowercase()); |
658 | 0 | canonicalize_unicode_extension_values(field.key, field.value, false); |
659 | 0 | } |
660 | |
|
661 | 0 | quick_sort(ext.fields, [](auto const& a, auto const& b) { return a.key < b.key; }); |
662 | 0 | }, |
663 | 0 | [&](OtherExtension& ext) { |
664 | 0 | ext.key = static_cast<char>(to_ascii_lowercase(ext.key)); |
665 | 0 | ext.value = MUST(ext.value.to_lowercase()); |
666 | 0 | }); |
667 | 0 | } |
668 | |
|
669 | 0 | for (auto& extension : locale_id.private_use_extensions) |
670 | 0 | extension = MUST(extension.to_lowercase()); |
671 | 0 | } |
672 | | |
673 | | Optional<String> canonicalize_unicode_locale_id(LocaleID& locale_id) |
674 | 0 | { |
675 | | // https://unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers |
676 | 0 | StringBuilder builder; |
677 | |
|
678 | 0 | auto append_sep_and_string = [&](Optional<String> const& string) { |
679 | 0 | if (!string.has_value() || string->is_empty()) |
680 | 0 | return; |
681 | 0 | builder.appendff("-{}", *string); |
682 | 0 | }; |
683 | |
|
684 | 0 | if (!locale_id.language_id.language.has_value()) |
685 | 0 | return {}; |
686 | | |
687 | 0 | transform_unicode_locale_id_to_canonical_syntax(locale_id); |
688 | |
|
689 | 0 | builder.append(MUST(locale_id.language_id.language->to_lowercase())); |
690 | 0 | append_sep_and_string(locale_id.language_id.script); |
691 | 0 | append_sep_and_string(locale_id.language_id.region); |
692 | 0 | for (auto const& variant : locale_id.language_id.variants) |
693 | 0 | append_sep_and_string(variant); |
694 | |
|
695 | 0 | for (auto const& extension : locale_id.extensions) { |
696 | 0 | extension.visit( |
697 | 0 | [&](LocaleExtension const& ext) { |
698 | 0 | builder.append("-u"sv); |
699 | |
|
700 | 0 | for (auto const& attribute : ext.attributes) |
701 | 0 | append_sep_and_string(attribute); |
702 | 0 | for (auto const& keyword : ext.keywords) { |
703 | 0 | append_sep_and_string(keyword.key); |
704 | 0 | append_sep_and_string(keyword.value); |
705 | 0 | } |
706 | 0 | }, |
707 | 0 | [&](TransformedExtension const& ext) { |
708 | 0 | builder.append("-t"sv); |
709 | |
|
710 | 0 | if (ext.language.has_value()) { |
711 | 0 | append_sep_and_string(ext.language->language); |
712 | 0 | append_sep_and_string(ext.language->script); |
713 | 0 | append_sep_and_string(ext.language->region); |
714 | 0 | for (auto const& variant : ext.language->variants) |
715 | 0 | append_sep_and_string(variant); |
716 | 0 | } |
717 | |
|
718 | 0 | for (auto const& field : ext.fields) { |
719 | 0 | append_sep_and_string(field.key); |
720 | 0 | append_sep_and_string(field.value); |
721 | 0 | } |
722 | 0 | }, |
723 | 0 | [&](OtherExtension const& ext) { |
724 | 0 | builder.appendff("-{:c}", to_ascii_lowercase(ext.key)); |
725 | 0 | append_sep_and_string(ext.value); |
726 | 0 | }); |
727 | 0 | } |
728 | |
|
729 | 0 | if (!locale_id.private_use_extensions.is_empty()) { |
730 | 0 | builder.append("-x"sv); |
731 | 0 | for (auto const& extension : locale_id.private_use_extensions) |
732 | 0 | append_sep_and_string(extension); |
733 | 0 | } |
734 | |
|
735 | 0 | return MUST(builder.to_string()); |
736 | 0 | } |
737 | | |
738 | | StringView default_locale() |
739 | 0 | { |
740 | 0 | return "en"sv; |
741 | 0 | } |
742 | | |
743 | | bool is_locale_available(StringView locale) |
744 | 0 | { |
745 | 0 | return locale_from_string(locale).has_value(); |
746 | 0 | } |
747 | | |
748 | | Style style_from_string(StringView style) |
749 | 0 | { |
750 | 0 | if (style == "narrow"sv) |
751 | 0 | return Style::Narrow; |
752 | 0 | if (style == "short"sv) |
753 | 0 | return Style::Short; |
754 | 0 | if (style == "long"sv) |
755 | 0 | return Style::Long; |
756 | 0 | VERIFY_NOT_REACHED(); |
757 | 0 | } |
758 | | |
759 | | StringView style_to_string(Style style) |
760 | 0 | { |
761 | 0 | switch (style) { |
762 | 0 | case Style::Narrow: |
763 | 0 | return "narrow"sv; |
764 | 0 | case Style::Short: |
765 | 0 | return "short"sv; |
766 | 0 | case Style::Long: |
767 | 0 | return "long"sv; |
768 | 0 | default: |
769 | 0 | VERIFY_NOT_REACHED(); |
770 | 0 | } |
771 | 0 | } |
772 | | |
773 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_keyword_values(StringView) { return {}; } |
774 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_calendars() { return {}; } |
775 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_collation_case_orderings() { return {}; } |
776 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_collation_numeric_orderings() { return {}; } |
777 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_collation_types() { return {}; } |
778 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_currencies() { return {}; } |
779 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_hour_cycles() { return {}; } |
780 | | ReadonlySpan<StringView> __attribute__((weak)) get_available_number_systems() { return {}; } |
781 | | Optional<Locale> __attribute__((weak)) locale_from_string(StringView) { return {}; } |
782 | | Optional<Language> __attribute__((weak)) language_from_string(StringView) { return {}; } |
783 | | Optional<Territory> __attribute__((weak)) territory_from_string(StringView) { return {}; } |
784 | | Optional<ScriptTag> __attribute__((weak)) script_tag_from_string(StringView) { return {}; } |
785 | | Optional<Currency> __attribute__((weak)) currency_from_string(StringView) { return {}; } |
786 | | Optional<DateField> __attribute__((weak)) date_field_from_string(StringView) { return {}; } |
787 | | Optional<ListPatternType> __attribute__((weak)) list_pattern_type_from_string(StringView) { return {}; } |
788 | | Optional<Key> __attribute__((weak)) key_from_string(StringView) { return {}; } |
789 | | Optional<KeywordCalendar> __attribute__((weak)) keyword_ca_from_string(StringView) { return {}; } |
790 | | Optional<KeywordCollation> __attribute__((weak)) keyword_co_from_string(StringView) { return {}; } |
791 | | Optional<KeywordHours> __attribute__((weak)) keyword_hc_from_string(StringView) { return {}; } |
792 | | Optional<KeywordColCaseFirst> __attribute__((weak)) keyword_kf_from_string(StringView) { return {}; } |
793 | | Optional<KeywordColNumeric> __attribute__((weak)) keyword_kn_from_string(StringView) { return {}; } |
794 | | Optional<KeywordNumbers> __attribute__((weak)) keyword_nu_from_string(StringView) { return {}; } |
795 | | Vector<StringView> __attribute__((weak)) get_keywords_for_locale(StringView, StringView) { return {}; } |
796 | | Optional<StringView> __attribute__((weak)) get_preferred_keyword_value_for_locale(StringView, StringView) { return {}; } |
797 | | Optional<DisplayPattern> __attribute__((weak)) get_locale_display_patterns(StringView) { return {}; } |
798 | | Optional<StringView> __attribute__((weak)) get_locale_language_mapping(StringView, StringView) { return {}; } |
799 | | Optional<StringView> __attribute__((weak)) get_locale_territory_mapping(StringView, StringView) { return {}; } |
800 | | Optional<StringView> __attribute__((weak)) get_locale_script_mapping(StringView, StringView) { return {}; } |
801 | | Optional<StringView> __attribute__((weak)) get_locale_long_currency_mapping(StringView, StringView) { return {}; } |
802 | | Optional<StringView> __attribute__((weak)) get_locale_short_currency_mapping(StringView, StringView) { return {}; } |
803 | | Optional<StringView> __attribute__((weak)) get_locale_narrow_currency_mapping(StringView, StringView) { return {}; } |
804 | | Optional<StringView> __attribute__((weak)) get_locale_numeric_currency_mapping(StringView, StringView) { return {}; } |
805 | | Optional<StringView> __attribute__((weak)) get_locale_calendar_mapping(StringView, StringView) { return {}; } |
806 | | Optional<StringView> __attribute__((weak)) get_locale_long_date_field_mapping(StringView, StringView) { return {}; } |
807 | | Optional<StringView> __attribute__((weak)) get_locale_short_date_field_mapping(StringView, StringView) { return {}; } |
808 | | Optional<StringView> __attribute__((weak)) get_locale_narrow_date_field_mapping(StringView, StringView) { return {}; } |
809 | | |
810 | | // https://www.unicode.org/reports/tr35/tr35-39/tr35-general.html#Display_Name_Elements |
811 | | Optional<String> format_locale_for_display(StringView locale, LocaleID locale_id) |
812 | 0 | { |
813 | 0 | auto language_id = move(locale_id.language_id); |
814 | 0 | VERIFY(language_id.language.has_value()); |
815 | | |
816 | 0 | auto patterns = get_locale_display_patterns(locale); |
817 | 0 | if (!patterns.has_value()) |
818 | 0 | return {}; |
819 | | |
820 | 0 | auto primary_tag = get_locale_language_mapping(locale, *language_id.language).value_or(*language_id.language); |
821 | 0 | Optional<StringView> script; |
822 | 0 | Optional<StringView> region; |
823 | |
|
824 | 0 | if (language_id.script.has_value()) |
825 | 0 | script = get_locale_script_mapping(locale, *language_id.script).value_or(*language_id.script); |
826 | 0 | if (language_id.region.has_value()) |
827 | 0 | region = get_locale_territory_mapping(locale, *language_id.region).value_or(*language_id.region); |
828 | |
|
829 | 0 | Optional<String> secondary_tag; |
830 | |
|
831 | 0 | if (script.has_value() && region.has_value()) { |
832 | 0 | secondary_tag = MUST(String::from_utf8(patterns->locale_separator)); |
833 | 0 | secondary_tag = MUST(secondary_tag->replace("{0}"sv, *script, ReplaceMode::FirstOnly)); |
834 | 0 | secondary_tag = MUST(secondary_tag->replace("{1}"sv, *region, ReplaceMode::FirstOnly)); |
835 | 0 | } else if (script.has_value()) { |
836 | 0 | secondary_tag = MUST(String::from_utf8(*script)); |
837 | 0 | } else if (region.has_value()) { |
838 | 0 | secondary_tag = MUST(String::from_utf8(*region)); |
839 | 0 | } |
840 | |
|
841 | 0 | if (!secondary_tag.has_value()) |
842 | 0 | return MUST(String::from_utf8(primary_tag)); |
843 | | |
844 | 0 | auto result = MUST(String::from_utf8(patterns->locale_pattern)); |
845 | 0 | result = MUST(result.replace("{0}"sv, primary_tag, ReplaceMode::FirstOnly)); |
846 | 0 | result = MUST(result.replace("{1}"sv, *secondary_tag, ReplaceMode::FirstOnly)); |
847 | |
|
848 | 0 | return result; |
849 | 0 | } |
850 | | |
851 | | Optional<ListPatterns> __attribute__((weak)) get_locale_list_patterns(StringView, StringView, Style) { return {}; } |
852 | | Optional<CharacterOrder> __attribute__((weak)) character_order_from_string(StringView) { return {}; } |
853 | | StringView __attribute__((weak)) character_order_to_string(CharacterOrder) { return {}; } |
854 | | Optional<CharacterOrder> __attribute__((weak)) character_order_for_locale(StringView) { return {}; } |
855 | | Optional<StringView> __attribute__((weak)) resolve_language_alias(StringView) { return {}; } |
856 | | Optional<StringView> __attribute__((weak)) resolve_territory_alias(StringView) { return {}; } |
857 | | Optional<StringView> __attribute__((weak)) resolve_script_tag_alias(StringView) { return {}; } |
858 | | Optional<StringView> __attribute__((weak)) resolve_variant_alias(StringView) { return {}; } |
859 | | Optional<StringView> __attribute__((weak)) resolve_subdivision_alias(StringView) { return {}; } |
860 | | void __attribute__((weak)) resolve_complex_language_aliases(LanguageID&) { } |
861 | | Optional<LanguageID> __attribute__((weak)) add_likely_subtags(LanguageID const&) { return {}; } |
862 | | |
863 | | Optional<LanguageID> remove_likely_subtags(LanguageID const& language_id) |
864 | 0 | { |
865 | | // https://www.unicode.org/reports/tr35/#Likely_Subtags |
866 | 0 | auto return_language_and_variants = [](auto language, auto variants) { |
867 | 0 | language.variants = move(variants); |
868 | 0 | return language; |
869 | 0 | }; |
870 | | |
871 | | // 1. First get max = AddLikelySubtags(inputLocale). If an error is signaled, return it. |
872 | 0 | auto maximized = add_likely_subtags(language_id); |
873 | 0 | if (!maximized.has_value()) |
874 | 0 | return {}; |
875 | | |
876 | | // 2. Remove the variants from max. |
877 | 0 | auto variants = move(maximized->variants); |
878 | | |
879 | | // 3. Get the components of the max (languagemax, scriptmax, regionmax). |
880 | 0 | auto language_max = maximized->language; |
881 | 0 | auto script_max = maximized->script; |
882 | 0 | auto region_max = maximized->region; |
883 | | |
884 | | // 4. Then for trial in {languagemax, languagemax_regionmax, languagemax_scriptmax}: |
885 | | // If AddLikelySubtags(trial) = max, then return trial + variants. |
886 | 0 | auto run_trial = [&](Optional<String> language, Optional<String> script, Optional<String> region) -> Optional<LanguageID> { |
887 | 0 | LanguageID trial { .language = move(language), .script = move(script), .region = move(region) }; |
888 | |
|
889 | 0 | if (add_likely_subtags(trial) == maximized) |
890 | 0 | return return_language_and_variants(move(trial), move(variants)); |
891 | 0 | return {}; |
892 | 0 | }; |
893 | |
|
894 | 0 | if (auto trial = run_trial(language_max, {}, {}); trial.has_value()) |
895 | 0 | return trial; |
896 | 0 | if (auto trial = run_trial(language_max, {}, region_max); trial.has_value()) |
897 | 0 | return trial; |
898 | 0 | if (auto trial = run_trial(language_max, script_max, {}); trial.has_value()) |
899 | 0 | return trial; |
900 | | |
901 | | // 5. If you do not get a match, return max + variants. |
902 | 0 | return return_language_and_variants(maximized.release_value(), move(variants)); |
903 | 0 | } |
904 | | |
905 | | Optional<String> __attribute__((weak)) resolve_most_likely_territory(LanguageID const&) { return {}; } |
906 | | |
907 | | String resolve_most_likely_territory_alias(LanguageID const& language_id, StringView territory_alias) |
908 | 0 | { |
909 | 0 | auto aliases = territory_alias.split_view(' '); |
910 | |
|
911 | 0 | if (aliases.size() > 1) { |
912 | 0 | auto territory = resolve_most_likely_territory(language_id); |
913 | 0 | if (territory.has_value() && aliases.contains_slow(*territory)) |
914 | 0 | return territory.release_value(); |
915 | 0 | } |
916 | | |
917 | 0 | return MUST(String::from_utf8(aliases[0])); |
918 | 0 | } |
919 | | |
920 | | String LanguageID::to_string() const |
921 | 0 | { |
922 | 0 | StringBuilder builder; |
923 | |
|
924 | 0 | auto append_segment = [&](Optional<String> const& segment) { |
925 | 0 | if (!segment.has_value()) |
926 | 0 | return; |
927 | 0 | if (!builder.is_empty()) |
928 | 0 | builder.append('-'); |
929 | 0 | builder.append(*segment); |
930 | 0 | }; |
931 | |
|
932 | 0 | append_segment(language); |
933 | 0 | append_segment(script); |
934 | 0 | append_segment(region); |
935 | 0 | for (auto const& variant : variants) |
936 | 0 | append_segment(variant); |
937 | |
|
938 | 0 | return MUST(builder.to_string()); |
939 | 0 | } |
940 | | |
941 | | String LocaleID::to_string() const |
942 | 0 | { |
943 | 0 | StringBuilder builder; |
944 | |
|
945 | 0 | auto append_segment = [&](auto const& segment) { |
946 | 0 | if (segment.is_empty()) |
947 | 0 | return; |
948 | 0 | if (!builder.is_empty()) |
949 | 0 | builder.append('-'); |
950 | 0 | builder.append(segment); |
951 | 0 | }; |
952 | |
|
953 | 0 | append_segment(language_id.to_string()); |
954 | |
|
955 | 0 | for (auto const& extension : extensions) { |
956 | 0 | extension.visit( |
957 | 0 | [&](LocaleExtension const& ext) { |
958 | 0 | builder.append("-u"sv); |
959 | 0 | for (auto const& attribute : ext.attributes) |
960 | 0 | append_segment(attribute); |
961 | 0 | for (auto const& keyword : ext.keywords) { |
962 | 0 | append_segment(keyword.key); |
963 | 0 | append_segment(keyword.value); |
964 | 0 | } |
965 | 0 | }, |
966 | 0 | [&](TransformedExtension const& ext) { |
967 | 0 | builder.append("-t"sv); |
968 | 0 | if (ext.language.has_value()) |
969 | 0 | append_segment(ext.language->to_string()); |
970 | 0 | for (auto const& field : ext.fields) { |
971 | 0 | append_segment(field.key); |
972 | 0 | append_segment(field.value); |
973 | 0 | } |
974 | 0 | }, |
975 | 0 | [&](OtherExtension const& ext) { |
976 | 0 | builder.appendff("-{}", ext.key); |
977 | 0 | append_segment(ext.value); |
978 | 0 | }); |
979 | 0 | } |
980 | |
|
981 | 0 | if (!private_use_extensions.is_empty()) { |
982 | 0 | builder.append("-x"sv); |
983 | 0 | for (auto const& extension : private_use_extensions) |
984 | 0 | append_segment(extension); |
985 | 0 | } |
986 | |
|
987 | 0 | return MUST(builder.to_string()); |
988 | 0 | } |
989 | | |
990 | | } |