| 1 | const std = @import("std"); |
| 2 | |
| 3 | /// This function is specific to how the Win32 RC command line interprets |
| 4 | /// language IDs specified as integers. |
| 5 | /// - Always interpreted as hexadecimal, but explicit 0x prefix is also allowed |
| 6 | /// - Wraps on overflow of u16 |
| 7 | /// - Stops parsing on any invalid hexadecimal digits |
| 8 | /// - Errors if a digit is not the first char |
| 9 | /// - `-` (negative) prefix is allowed |
| 10 | pub fn parseInt(str: []const u8) error{InvalidLanguageId}!u16 { |
| 11 | var result: u16 = 0; |
| 12 | const radix: u8 = 16; |
| 13 | var buf = str; |
| 14 | |
| 15 | const Prefix = enum { none, minus }; |
| 16 | var prefix: Prefix = .none; |
| 17 | switch (buf[0]) { |
| 18 | '-' => { |
| 19 | prefix = .minus; |
| 20 | buf = buf[1..]; |
| 21 | }, |
| 22 | else => {}, |
| 23 | } |
| 24 | |
| 25 | if (buf.len > 2 and buf[0] == '0' and buf[1] == 'x') { |
| 26 | buf = buf[2..]; |
| 27 | } |
| 28 | |
| 29 | for (buf, 0..) |c, i| { |
| 30 | const digit = switch (c) { |
| 31 | // On invalid digit for the radix, just stop parsing but don't fail |
| 32 | 'a'...'f', 'A'...'F', '0'...'9' => std.fmt.charToDigit(c, radix) catch break, |
| 33 | else => { |
| 34 | // First digit must be valid |
| 35 | if (i == 0) { |
| 36 | return error.InvalidLanguageId; |
| 37 | } |
| 38 | break; |
| 39 | }, |
| 40 | }; |
| 41 | |
| 42 | if (result != 0) { |
| 43 | result *%= radix; |
| 44 | } |
| 45 | result +%= digit; |
| 46 | } |
| 47 | |
| 48 | switch (prefix) { |
| 49 | .none => {}, |
| 50 | .minus => result = 0 -% result, |
| 51 | } |
| 52 | |
| 53 | return result; |
| 54 | } |
| 55 | |
| 56 | test parseInt { |
| 57 | try std.testing.expectEqual(@as(u16, 0x16), try parseInt("16")); |
| 58 | try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1A")); |
| 59 | try std.testing.expectEqual(@as(u16, 0x1a), try parseInt("0x1Azzzz")); |
| 60 | try std.testing.expectEqual(@as(u16, 0xffff), try parseInt("-1")); |
| 61 | try std.testing.expectEqual(@as(u16, 0xffea), try parseInt("-0x16")); |
| 62 | try std.testing.expectEqual(@as(u16, 0x0), try parseInt("0o100")); |
| 63 | try std.testing.expectEqual(@as(u16, 0x1), try parseInt("10001")); |
| 64 | try std.testing.expectError(error.InvalidLanguageId, parseInt("--1")); |
| 65 | try std.testing.expectError(error.InvalidLanguageId, parseInt("0xha")); |
| 66 | try std.testing.expectError(error.InvalidLanguageId, parseInt("¹")); |
| 67 | try std.testing.expectError(error.InvalidLanguageId, parseInt("~1")); |
| 68 | } |
| 69 | |
| 70 | /// This function is specific to how the Win32 RC command line interprets |
| 71 | /// language tags: invalid tags are rejected, but tags that don't have |
| 72 | /// a specific assigned ID but are otherwise valid enough will get |
| 73 | /// converted to an ID of LOCALE_CUSTOM_UNSPECIFIED. |
| 74 | pub fn tagToInt(tag: []const u8) error{InvalidLanguageTag}!u16 { |
| 75 | const maybe_id = try tagToId(tag); |
| 76 | if (maybe_id) |id| { |
| 77 | return @backingInt(id); |
| 78 | } else { |
| 79 | return LOCALE_CUSTOM_UNSPECIFIED; |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | pub fn tagToId(tag: []const u8) error{InvalidLanguageTag}!?LanguageId { |
| 84 | const parsed = try parse(tag); |
| 85 | // There are currently no language tags with assigned IDs that have |
| 86 | // multiple suffixes, so we can skip the lookup. |
| 87 | if (parsed.multiple_suffixes) return null; |
| 88 | const longest_known_tag = comptime blk: { |
| 89 | var len = 0; |
| 90 | for (@typeInfo(LanguageId).@"enum".field_names) |field_name| { |
| 91 | if (field_name.len > len) len = field_name.len; |
| 92 | } |
| 93 | break :blk len; |
| 94 | }; |
| 95 | // If the tag is longer than the longest tag that has an assigned ID, |
| 96 | // then we can skip the lookup. |
| 97 | if (tag.len > longest_known_tag) return null; |
| 98 | var normalized_buf: [longest_known_tag]u8 = undefined; |
| 99 | // To allow e.g. `de-de_phoneb` to get looked up as `de-de`, we need to |
| 100 | // omit the suffix, but only if the tag contains a valid alternate sort order. |
| 101 | const tag_to_normalize = if (parsed.isSuffixValidSortOrder()) tag[0 .. tag.len - (parsed.suffix.?.len + 1)] else tag; |
| 102 | const normalized_tag = normalizeTag(tag_to_normalize, &normalized_buf); |
| 103 | return std.meta.stringToEnum(LanguageId, normalized_tag) orelse { |
| 104 | // special case for a tag that has been mapped to the same ID |
| 105 | // twice. |
| 106 | if (std.mem.eql(u8, "ff_latn_ng", normalized_tag)) { |
| 107 | return LanguageId.ff_ng; |
| 108 | } |
| 109 | return null; |
| 110 | }; |
| 111 | } |
| 112 | |
| 113 | test tagToId { |
| 114 | try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("ar-ae")).?); |
| 115 | try std.testing.expectEqual(LanguageId.ar_ae, (try tagToId("AR_AE")).?); |
| 116 | try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-ng")).?); |
| 117 | // Special case |
| 118 | try std.testing.expectEqual(LanguageId.ff_ng, (try tagToId("ff-Latn-NG")).?); |
| 119 | } |
| 120 | |
| 121 | test "exhaustive tagToId" { |
| 122 | @setEvalBranchQuota(2000); |
| 123 | inline for (@typeInfo(LanguageId).@"enum".field_names) |field_name| { |
| 124 | const id = tagToId(field_name) catch |err| { |
| 125 | std.debug.print("tag: {s}\n", .{field_name}); |
| 126 | return err; |
| 127 | }; |
| 128 | try std.testing.expectEqual(@field(LanguageId, field_name), id orelse { |
| 129 | std.debug.print("tag: {s}, got null\n", .{field_name}); |
| 130 | return error.TestExpectedEqual; |
| 131 | }); |
| 132 | } |
| 133 | var buf: [32]u8 = undefined; |
| 134 | inline for (valid_alternate_sorts) |parsed_sort| { |
| 135 | var fbs: std.Io.Writer = .fixed(&buf); |
| 136 | const writer = &fbs; |
| 137 | writer.writeAll(parsed_sort.language_code) catch unreachable; |
| 138 | writer.writeAll("-") catch unreachable; |
| 139 | writer.writeAll(parsed_sort.country_code.?) catch unreachable; |
| 140 | writer.writeAll("-") catch unreachable; |
| 141 | writer.writeAll(parsed_sort.suffix.?) catch unreachable; |
| 142 | const expected_field_name = comptime field: { |
| 143 | var name_buf: [5]u8 = undefined; |
| 144 | @memcpy(name_buf[0..parsed_sort.language_code.len], parsed_sort.language_code); |
| 145 | name_buf[2] = '_'; |
| 146 | @memcpy(name_buf[3..], parsed_sort.country_code.?); |
| 147 | break :field name_buf; |
| 148 | }; |
| 149 | const expected = @field(LanguageId, &expected_field_name); |
| 150 | const id = tagToId(fbs.buffered()) catch |err| { |
| 151 | std.debug.print("tag: {s}\n", .{fbs.buffered()}); |
| 152 | return err; |
| 153 | }; |
| 154 | try std.testing.expectEqual(expected, id orelse { |
| 155 | std.debug.print("tag: {s}, expected: {}, got null\n", .{ fbs.buffered(), expected }); |
| 156 | return error.TestExpectedEqual; |
| 157 | }); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | fn normalizeTag(tag: []const u8, buf: []u8) []u8 { |
| 162 | std.debug.assert(buf.len >= tag.len); |
| 163 | for (tag, 0..) |c, i| { |
| 164 | if (c == '-') |
| 165 | buf[i] = '_' |
| 166 | else |
| 167 | buf[i] = std.ascii.toLower(c); |
| 168 | } |
| 169 | return buf[0..tag.len]; |
| 170 | } |
| 171 | |
| 172 | /// https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-LCID/%5bMS-LCID%5d.pdf#%5B%7B%22num%22%3A72%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C69%2C574%2C0%5D |
| 173 | /// "When an LCID is requested for a locale without a |
| 174 | /// permanent LCID assignment, nor a temporary |
| 175 | /// assignment as above, the protocol will respond |
| 176 | /// with LOCALE_CUSTOM_UNSPECIFIED for all such |
| 177 | /// locales. Because this single value is used for |
| 178 | /// numerous possible locale names, it is impossible to |
| 179 | /// round trip this locale, even temporarily. |
| 180 | /// Applications should discard this value as soon as |
| 181 | /// possible and never persist it. If the system is |
| 182 | /// forced to respond to a request for |
| 183 | /// LCID_CUSTOM_UNSPECIFIED, it will fall back to |
| 184 | /// the current user locale. This is often incorrect but |
| 185 | /// may prevent an application or component from |
| 186 | /// failing. As the meaning of this temporary LCID is |
| 187 | /// unstable, it should never be used for interchange |
| 188 | /// or persisted data. This is a 1-to-many relationship |
| 189 | /// that is very unstable." |
| 190 | pub const LOCALE_CUSTOM_UNSPECIFIED = 0x1000; |
| 191 | |
| 192 | pub const LANG_ENGLISH = 0x09; |
| 193 | pub const SUBLANG_ENGLISH_US = 0x01; |
| 194 | |
| 195 | /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers |
| 196 | pub fn MAKELANGID(primary: u10, sublang: u6) u16 { |
| 197 | return (@as(u16, primary) << 10) | sublang; |
| 198 | } |
| 199 | |
| 200 | /// Language tag format expressed as a regular expression (rough approximation): |
| 201 | /// |
| 202 | /// [a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})? |
| 203 | /// lang | script | country | suffix |
| 204 | /// |
| 205 | /// Notes: |
| 206 | /// - If lang code is 1 char, it seems to mean that everything afterwards uses suffix |
| 207 | /// parsing rules (e.g. `a-0` and `a-00000000` are allowed). |
| 208 | /// - There can also be any number of trailing suffix parts as long as they each |
| 209 | /// would be a valid suffix part, e.g. `en-us-blah-blah1-blah2-blah3` is allowed. |
| 210 | /// - When doing lookups, trailing suffix parts are taken into account, e.g. |
| 211 | /// `ca-es-valencia` is not considered equivalent to `ca-es-valencia-blah`. |
| 212 | /// - A suffix is only allowed if: |
| 213 | /// + Lang code is 1 char long, or |
| 214 | /// + A country code is present, or |
| 215 | /// + A script tag is not present and: |
| 216 | /// - the suffix is numeric-only and has a length of 3, or |
| 217 | /// - the lang is `qps` and the suffix is `ploca` or `plocm` |
| 218 | pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed { |
| 219 | var it = std.mem.splitAny(u8, lang_tag, "-_"); |
| 220 | const lang_code = it.first(); |
| 221 | const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code); |
| 222 | if (!is_valid_lang_code) return error.InvalidLanguageTag; |
| 223 | var parsed = Parsed{ |
| 224 | .language_code = lang_code, |
| 225 | }; |
| 226 | // The second part could be a script tag, a country code, or a suffix |
| 227 | if (it.next()) |part_str| { |
| 228 | // The lang code being length 1 behaves strangely, so fully special case it. |
| 229 | if (lang_code.len == 1) { |
| 230 | // This is almost certainly not the 'right' way to do this, but I don't have a method |
| 231 | // to determine how exactly these language tags are parsed, and it seems like |
| 232 | // suffix parsing rules apply generally (digits allowed, length of 1 to 8). |
| 233 | // |
| 234 | // However, because we want to be able to lookup `x-iv-mathan` normally without |
| 235 | // `multiple_suffixes` being set to true, we need to make sure to treat two-length |
| 236 | // alphabetic parts as a country code. |
| 237 | if (part_str.len == 2 and isAllAlphabetic(part_str)) { |
| 238 | parsed.country_code = part_str; |
| 239 | } |
| 240 | // Everything else, though, we can just throw into the suffix as long as the normal |
| 241 | // rules apply. |
| 242 | else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) { |
| 243 | parsed.suffix = part_str; |
| 244 | } else { |
| 245 | return error.InvalidLanguageTag; |
| 246 | } |
| 247 | } else if (part_str.len == 4 and isAllAlphabetic(part_str)) { |
| 248 | parsed.script_tag = part_str; |
| 249 | } else if (part_str.len == 2 and isAllAlphabetic(part_str)) { |
| 250 | parsed.country_code = part_str; |
| 251 | } |
| 252 | // Only a 3-len numeric suffix is allowed as the second part of a tag |
| 253 | else if (part_str.len == 3 and isAllNumeric(part_str)) { |
| 254 | parsed.suffix = part_str; |
| 255 | } |
| 256 | // Special case for qps-ploca and qps-plocm |
| 257 | else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and |
| 258 | (std.ascii.eqlIgnoreCase(part_str, "ploca") or |
| 259 | std.ascii.eqlIgnoreCase(part_str, "plocm"))) |
| 260 | { |
| 261 | parsed.suffix = part_str; |
| 262 | } else { |
| 263 | return error.InvalidLanguageTag; |
| 264 | } |
| 265 | } else { |
| 266 | // If there's no part besides a 1-len lang code, then it is malformed |
| 267 | if (lang_code.len == 1) return error.InvalidLanguageTag; |
| 268 | return parsed; |
| 269 | } |
| 270 | if (parsed.script_tag != null) { |
| 271 | if (it.next()) |part_str| { |
| 272 | if (part_str.len == 2 and isAllAlphabetic(part_str)) { |
| 273 | parsed.country_code = part_str; |
| 274 | } else { |
| 275 | // Suffix is not allowed when a country code is not present. |
| 276 | return error.InvalidLanguageTag; |
| 277 | } |
| 278 | } else { |
| 279 | return parsed; |
| 280 | } |
| 281 | } |
| 282 | // We've now parsed any potential script tag/country codes, so anything remaining |
| 283 | // is a suffix |
| 284 | while (it.next()) |part_str| { |
| 285 | if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) { |
| 286 | return error.InvalidLanguageTag; |
| 287 | } |
| 288 | if (parsed.suffix == null) { |
| 289 | parsed.suffix = part_str; |
| 290 | } else { |
| 291 | // In theory we could return early here but we still want to validate |
| 292 | // that each part is a valid suffix all the way to the end, e.g. |
| 293 | // we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!` |
| 294 | // suffix part. |
| 295 | parsed.multiple_suffixes = true; |
| 296 | } |
| 297 | } |
| 298 | return parsed; |
| 299 | } |
| 300 | |
| 301 | pub const Parsed = struct { |
| 302 | language_code: []const u8, |
| 303 | script_tag: ?[]const u8 = null, |
| 304 | country_code: ?[]const u8 = null, |
| 305 | /// Can be a sort order (e.g. phoneb) or something like valencia, 001, etc |
| 306 | suffix: ?[]const u8 = null, |
| 307 | /// There can be any number of suffixes, but we don't need to care what their |
| 308 | /// values are, we just need to know if any exist so that e.g. `ca-es-valencia-blah` |
| 309 | /// can be seen as different from `ca-es-valencia`. Storing this as a bool |
| 310 | /// allows us to avoid needing either (a) dynamic allocation or (b) a limit to |
| 311 | /// the number of suffixes allowed when parsing. |
| 312 | multiple_suffixes: bool = false, |
| 313 | |
| 314 | pub fn isSuffixValidSortOrder(self: Parsed) bool { |
| 315 | if (self.country_code == null) return false; |
| 316 | if (self.suffix == null) return false; |
| 317 | if (self.script_tag != null) return false; |
| 318 | if (self.multiple_suffixes) return false; |
| 319 | for (valid_alternate_sorts) |valid_sort| { |
| 320 | if (std.ascii.eqlIgnoreCase(valid_sort.language_code, self.language_code) and |
| 321 | std.ascii.eqlIgnoreCase(valid_sort.country_code.?, self.country_code.?) and |
| 322 | std.ascii.eqlIgnoreCase(valid_sort.suffix.?, self.suffix.?)) |
| 323 | { |
| 324 | return true; |
| 325 | } |
| 326 | } |
| 327 | return false; |
| 328 | } |
| 329 | }; |
| 330 | |
| 331 | /// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f |
| 332 | /// See the table following this text: "Alternate sorts can be selected by using one of the identifiers from the following table." |
| 333 | const valid_alternate_sorts = [_]Parsed{ |
| 334 | // Note: x-IV-mathan is omitted due to how lookups are implemented. |
| 335 | // This table is used to make e.g. `de-de_phoneb` get looked up |
| 336 | // as `de-de` (the suffix is omitted for the lookup), but x-iv-mathan |
| 337 | // instead needs to be looked up with the suffix included because |
| 338 | // `x-iv` is not a tag with an assigned ID. |
| 339 | .{ .language_code = "de", .country_code = "de", .suffix = "phoneb" }, |
| 340 | .{ .language_code = "hu", .country_code = "hu", .suffix = "tchncl" }, |
| 341 | .{ .language_code = "ka", .country_code = "ge", .suffix = "modern" }, |
| 342 | .{ .language_code = "zh", .country_code = "cn", .suffix = "stroke" }, |
| 343 | .{ .language_code = "zh", .country_code = "sg", .suffix = "stroke" }, |
| 344 | .{ .language_code = "zh", .country_code = "mo", .suffix = "stroke" }, |
| 345 | .{ .language_code = "zh", .country_code = "tw", .suffix = "pronun" }, |
| 346 | .{ .language_code = "zh", .country_code = "tw", .suffix = "radstr" }, |
| 347 | .{ .language_code = "ja", .country_code = "jp", .suffix = "radstr" }, |
| 348 | .{ .language_code = "zh", .country_code = "hk", .suffix = "radstr" }, |
| 349 | .{ .language_code = "zh", .country_code = "mo", .suffix = "radstr" }, |
| 350 | .{ .language_code = "zh", .country_code = "cn", .suffix = "phoneb" }, |
| 351 | .{ .language_code = "zh", .country_code = "sg", .suffix = "phoneb" }, |
| 352 | }; |
| 353 | |
| 354 | test "parse" { |
| 355 | try std.testing.expectEqualDeep(Parsed{ |
| 356 | .language_code = "en", |
| 357 | }, try parse("en")); |
| 358 | try std.testing.expectEqualDeep(Parsed{ |
| 359 | .language_code = "en", |
| 360 | .country_code = "us", |
| 361 | }, try parse("en-us")); |
| 362 | try std.testing.expectEqualDeep(Parsed{ |
| 363 | .language_code = "en", |
| 364 | .suffix = "123", |
| 365 | }, try parse("en-123")); |
| 366 | try std.testing.expectEqualDeep(Parsed{ |
| 367 | .language_code = "en", |
| 368 | .suffix = "123", |
| 369 | .multiple_suffixes = true, |
| 370 | }, try parse("en-123-blah")); |
| 371 | try std.testing.expectEqualDeep(Parsed{ |
| 372 | .language_code = "en", |
| 373 | .country_code = "us", |
| 374 | .suffix = "123", |
| 375 | .multiple_suffixes = true, |
| 376 | }, try parse("en-us_123-blah")); |
| 377 | try std.testing.expectEqualDeep(Parsed{ |
| 378 | .language_code = "eng", |
| 379 | .script_tag = "Latn", |
| 380 | }, try parse("eng-Latn")); |
| 381 | try std.testing.expectEqualDeep(Parsed{ |
| 382 | .language_code = "eng", |
| 383 | .script_tag = "Latn", |
| 384 | }, try parse("eng-Latn")); |
| 385 | try std.testing.expectEqualDeep(Parsed{ |
| 386 | .language_code = "ff", |
| 387 | .script_tag = "Latn", |
| 388 | .country_code = "NG", |
| 389 | }, try parse("ff-Latn-NG")); |
| 390 | try std.testing.expectEqualDeep(Parsed{ |
| 391 | .language_code = "qps", |
| 392 | .suffix = "Plocm", |
| 393 | }, try parse("qps-Plocm")); |
| 394 | try std.testing.expectEqualDeep(Parsed{ |
| 395 | .language_code = "qps", |
| 396 | .suffix = "ploca", |
| 397 | }, try parse("qps-ploca")); |
| 398 | try std.testing.expectEqualDeep(Parsed{ |
| 399 | .language_code = "x", |
| 400 | .country_code = "IV", |
| 401 | .suffix = "mathan", |
| 402 | }, try parse("x-IV-mathan")); |
| 403 | try std.testing.expectEqualDeep(Parsed{ |
| 404 | .language_code = "a", |
| 405 | .suffix = "a", |
| 406 | }, try parse("a-a")); |
| 407 | try std.testing.expectEqualDeep(Parsed{ |
| 408 | .language_code = "a", |
| 409 | .suffix = "000", |
| 410 | }, try parse("a-000")); |
| 411 | try std.testing.expectEqualDeep(Parsed{ |
| 412 | .language_code = "a", |
| 413 | .suffix = "00000000", |
| 414 | }, try parse("a-00000000")); |
| 415 | // suffix not allowed if script tag is present without country code |
| 416 | try std.testing.expectError(error.InvalidLanguageTag, parse("eng-Latn-suffix")); |
| 417 | // suffix must be 3 numeric digits if neither script tag nor country code is present |
| 418 | try std.testing.expectError(error.InvalidLanguageTag, parse("eng-suffix")); |
| 419 | try std.testing.expectError(error.InvalidLanguageTag, parse("en-plocm")); |
| 420 | // 1-len lang code is not allowed if it's the only part |
| 421 | try std.testing.expectError(error.InvalidLanguageTag, parse("e")); |
| 422 | } |
| 423 | |
| 424 | fn isAllAlphabetic(str: []const u8) bool { |
| 425 | for (str) |c| { |
| 426 | if (!std.ascii.isAlphabetic(c)) return false; |
| 427 | } |
| 428 | return true; |
| 429 | } |
| 430 | |
| 431 | fn isAllAlphanumeric(str: []const u8) bool { |
| 432 | for (str) |c| { |
| 433 | if (!std.ascii.isAlphanumeric(c)) return false; |
| 434 | } |
| 435 | return true; |
| 436 | } |
| 437 | |
| 438 | fn isAllNumeric(str: []const u8) bool { |
| 439 | for (str) |c| { |
| 440 | if (!std.ascii.isDigit(c)) return false; |
| 441 | } |
| 442 | return true; |
| 443 | } |
| 444 | |
| 445 | /// Derived from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f |
| 446 | /// - Protocol Revision: 15.0 |
| 447 | /// - Language / Language ID / Language Tag table in Appendix A |
| 448 | /// - Removed all rows that have Language ID 0x1000 (LOCALE_CUSTOM_UNSPECIFIED) |
| 449 | /// - Normalized each language tag (lowercased, replaced all `-` with `_`) |
| 450 | /// - There is one special case where two tags are mapped to the same ID, the following |
| 451 | /// has been omitted and must be special cased during lookup to map to the ID ff_ng / 0x0467. |
| 452 | /// ff_latn_ng = 0x0467, // Fulah (Latin), Nigeria |
| 453 | /// - x_iv_mathan has been added which is not in the table but does appear in the Alternate sorts |
| 454 | /// table as 0x007F (LANG_INVARIANT). |
| 455 | pub const LanguageId = enum(u16) { |
| 456 | // Language tag = Language ID, // Language, Location (or type) |
| 457 | af = 0x0036, // Afrikaans |
| 458 | af_za = 0x0436, // Afrikaans, South Africa |
| 459 | sq = 0x001C, // Albanian |
| 460 | sq_al = 0x041C, // Albanian, Albania |
| 461 | gsw = 0x0084, // Alsatian |
| 462 | gsw_fr = 0x0484, // Alsatian, France |
| 463 | am = 0x005E, // Amharic |
| 464 | am_et = 0x045E, // Amharic, Ethiopia |
| 465 | ar = 0x0001, // Arabic |
| 466 | ar_dz = 0x1401, // Arabic, Algeria |
| 467 | ar_bh = 0x3C01, // Arabic, Bahrain |
| 468 | ar_eg = 0x0c01, // Arabic, Egypt |
| 469 | ar_iq = 0x0801, // Arabic, Iraq |
| 470 | ar_jo = 0x2C01, // Arabic, Jordan |
| 471 | ar_kw = 0x3401, // Arabic, Kuwait |
| 472 | ar_lb = 0x3001, // Arabic, Lebanon |
| 473 | ar_ly = 0x1001, // Arabic, Libya |
| 474 | ar_ma = 0x1801, // Arabic, Morocco |
| 475 | ar_om = 0x2001, // Arabic, Oman |
| 476 | ar_qa = 0x4001, // Arabic, Qatar |
| 477 | ar_sa = 0x0401, // Arabic, Saudi Arabia |
| 478 | ar_sy = 0x2801, // Arabic, Syria |
| 479 | ar_tn = 0x1C01, // Arabic, Tunisia |
| 480 | ar_ae = 0x3801, // Arabic, U.A.E. |
| 481 | ar_ye = 0x2401, // Arabic, Yemen |
| 482 | hy = 0x002B, // Armenian |
| 483 | hy_am = 0x042B, // Armenian, Armenia |
| 484 | as = 0x004D, // Assamese |
| 485 | as_in = 0x044D, // Assamese, India |
| 486 | az_cyrl = 0x742C, // Azerbaijani (Cyrillic) |
| 487 | az_cyrl_az = 0x082C, // Azerbaijani (Cyrillic), Azerbaijan |
| 488 | az = 0x002C, // Azerbaijani (Latin) |
| 489 | az_latn = 0x782C, // Azerbaijani (Latin) |
| 490 | az_latn_az = 0x042C, // Azerbaijani (Latin), Azerbaijan |
| 491 | bn = 0x0045, // Bangla |
| 492 | bn_bd = 0x0845, // Bangla, Bangladesh |
| 493 | bn_in = 0x0445, // Bangla, India |
| 494 | ba = 0x006D, // Bashkir |
| 495 | ba_ru = 0x046D, // Bashkir, Russia |
| 496 | eu = 0x002D, // Basque |
| 497 | eu_es = 0x042D, // Basque, Spain |
| 498 | be = 0x0023, // Belarusian |
| 499 | be_by = 0x0423, // Belarusian, Belarus |
| 500 | bs_cyrl = 0x641A, // Bosnian (Cyrillic) |
| 501 | bs_cyrl_ba = 0x201A, // Bosnian (Cyrillic), Bosnia and Herzegovina |
| 502 | bs_latn = 0x681A, // Bosnian (Latin) |
| 503 | bs = 0x781A, // Bosnian (Latin) |
| 504 | bs_latn_ba = 0x141A, // Bosnian (Latin), Bosnia and Herzegovina |
| 505 | br = 0x007E, // Breton |
| 506 | br_fr = 0x047E, // Breton, France |
| 507 | bg = 0x0002, // Bulgarian |
| 508 | bg_bg = 0x0402, // Bulgarian, Bulgaria |
| 509 | my = 0x0055, // Burmese |
| 510 | my_mm = 0x0455, // Burmese, Myanmar |
| 511 | ca = 0x0003, // Catalan |
| 512 | ca_es = 0x0403, // Catalan, Spain |
| 513 | tzm_arab_ma = 0x045F, // Central Atlas Tamazight (Arabic), Morocco |
| 514 | ku = 0x0092, // Central Kurdish |
| 515 | ku_arab = 0x7c92, // Central Kurdish |
| 516 | ku_arab_iq = 0x0492, // Central Kurdish, Iraq |
| 517 | chr = 0x005C, // Cherokee |
| 518 | chr_cher = 0x7c5C, // Cherokee |
| 519 | chr_cher_us = 0x045C, // Cherokee, United States |
| 520 | zh_hans = 0x0004, // Chinese (Simplified) |
| 521 | zh = 0x7804, // Chinese (Simplified) |
| 522 | zh_cn = 0x0804, // Chinese (Simplified), People's Republic of China |
| 523 | zh_sg = 0x1004, // Chinese (Simplified), Singapore |
| 524 | zh_hant = 0x7C04, // Chinese (Traditional) |
| 525 | zh_hk = 0x0C04, // Chinese (Traditional), Hong Kong S.A.R. |
| 526 | zh_mo = 0x1404, // Chinese (Traditional), Macao S.A.R. |
| 527 | zh_tw = 0x0404, // Chinese (Traditional), Taiwan |
| 528 | co = 0x0083, // Corsican |
| 529 | co_fr = 0x0483, // Corsican, France |
| 530 | hr = 0x001A, // Croatian |
| 531 | hr_hr = 0x041A, // Croatian, Croatia |
| 532 | hr_ba = 0x101A, // Croatian (Latin), Bosnia and Herzegovina |
| 533 | cs = 0x0005, // Czech |
| 534 | cs_cz = 0x0405, // Czech, Czech Republic |
| 535 | da = 0x0006, // Danish |
| 536 | da_dk = 0x0406, // Danish, Denmark |
| 537 | prs = 0x008C, // Dari |
| 538 | prs_af = 0x048C, // Dari, Afghanistan |
| 539 | dv = 0x0065, // Divehi |
| 540 | dv_mv = 0x0465, // Divehi, Maldives |
| 541 | nl = 0x0013, // Dutch |
| 542 | nl_be = 0x0813, // Dutch, Belgium |
| 543 | nl_nl = 0x0413, // Dutch, Netherlands |
| 544 | dz_bt = 0x0C51, // Dzongkha, Bhutan |
| 545 | en = 0x0009, // English |
| 546 | en_au = 0x0C09, // English, Australia |
| 547 | en_bz = 0x2809, // English, Belize |
| 548 | en_ca = 0x1009, // English, Canada |
| 549 | en_029 = 0x2409, // English, Caribbean |
| 550 | en_hk = 0x3C09, // English, Hong Kong |
| 551 | en_in = 0x4009, // English, India |
| 552 | en_ie = 0x1809, // English, Ireland |
| 553 | en_jm = 0x2009, // English, Jamaica |
| 554 | en_my = 0x4409, // English, Malaysia |
| 555 | en_nz = 0x1409, // English, New Zealand |
| 556 | en_ph = 0x3409, // English, Republic of the Philippines |
| 557 | en_sg = 0x4809, // English, Singapore |
| 558 | en_za = 0x1C09, // English, South Africa |
| 559 | en_tt = 0x2c09, // English, Trinidad and Tobago |
| 560 | en_ae = 0x4C09, // English, United Arab Emirates |
| 561 | en_gb = 0x0809, // English, United Kingdom |
| 562 | en_us = 0x0409, // English, United States |
| 563 | en_zw = 0x3009, // English, Zimbabwe |
| 564 | et = 0x0025, // Estonian |
| 565 | et_ee = 0x0425, // Estonian, Estonia |
| 566 | fo = 0x0038, // Faroese |
| 567 | fo_fo = 0x0438, // Faroese, Faroe Islands |
| 568 | fil = 0x0064, // Filipino |
| 569 | fil_ph = 0x0464, // Filipino, Philippines |
| 570 | fi = 0x000B, // Finnish |
| 571 | fi_fi = 0x040B, // Finnish, Finland |
| 572 | fr = 0x000C, // French |
| 573 | fr_be = 0x080C, // French, Belgium |
| 574 | fr_cm = 0x2c0C, // French, Cameroon |
| 575 | fr_ca = 0x0c0C, // French, Canada |
| 576 | fr_029 = 0x1C0C, // French, Caribbean |
| 577 | fr_cd = 0x240C, // French, Congo, DRC |
| 578 | fr_ci = 0x300C, // French, Côte d'Ivoire |
| 579 | fr_fr = 0x040C, // French, France |
| 580 | fr_ht = 0x3c0C, // French, Haiti |
| 581 | fr_lu = 0x140C, // French, Luxembourg |
| 582 | fr_ml = 0x340C, // French, Mali |
| 583 | fr_ma = 0x380C, // French, Morocco |
| 584 | fr_mc = 0x180C, // French, Principality of Monaco |
| 585 | fr_re = 0x200C, // French, Reunion |
| 586 | fr_sn = 0x280C, // French, Senegal |
| 587 | fr_ch = 0x100C, // French, Switzerland |
| 588 | fy = 0x0062, // Frisian |
| 589 | fy_nl = 0x0462, // Frisian, Netherlands |
| 590 | ff = 0x0067, // Fulah |
| 591 | ff_latn = 0x7C67, // Fulah (Latin) |
| 592 | ff_ng = 0x0467, // Fulah, Nigeria |
| 593 | ff_latn_sn = 0x0867, // Fulah, Senegal |
| 594 | gl = 0x0056, // Galician |
| 595 | gl_es = 0x0456, // Galician, Spain |
| 596 | ka = 0x0037, // Georgian |
| 597 | ka_ge = 0x0437, // Georgian, Georgia |
| 598 | de = 0x0007, // German |
| 599 | de_at = 0x0C07, // German, Austria |
| 600 | de_de = 0x0407, // German, Germany |
| 601 | de_li = 0x1407, // German, Liechtenstein |
| 602 | de_lu = 0x1007, // German, Luxembourg |
| 603 | de_ch = 0x0807, // German, Switzerland |
| 604 | el = 0x0008, // Greek |
| 605 | el_gr = 0x0408, // Greek, Greece |
| 606 | kl = 0x006F, // Greenlandic |
| 607 | kl_gl = 0x046F, // Greenlandic, Greenland |
| 608 | gn = 0x0074, // Guarani |
| 609 | gn_py = 0x0474, // Guarani, Paraguay |
| 610 | gu = 0x0047, // Gujarati |
| 611 | gu_in = 0x0447, // Gujarati, India |
| 612 | ha = 0x0068, // Hausa (Latin) |
| 613 | ha_latn = 0x7C68, // Hausa (Latin) |
| 614 | ha_latn_ng = 0x0468, // Hausa (Latin), Nigeria |
| 615 | haw = 0x0075, // Hawaiian |
| 616 | haw_us = 0x0475, // Hawaiian, United States |
| 617 | he = 0x000D, // Hebrew |
| 618 | he_il = 0x040D, // Hebrew, Israel |
| 619 | hi = 0x0039, // Hindi |
| 620 | hi_in = 0x0439, // Hindi, India |
| 621 | hu = 0x000E, // Hungarian |
| 622 | hu_hu = 0x040E, // Hungarian, Hungary |
| 623 | is = 0x000F, // Icelandic |
| 624 | is_is = 0x040F, // Icelandic, Iceland |
| 625 | ig = 0x0070, // Igbo |
| 626 | ig_ng = 0x0470, // Igbo, Nigeria |
| 627 | id = 0x0021, // Indonesian |
| 628 | id_id = 0x0421, // Indonesian, Indonesia |
| 629 | iu = 0x005D, // Inuktitut (Latin) |
| 630 | iu_latn = 0x7C5D, // Inuktitut (Latin) |
| 631 | iu_latn_ca = 0x085D, // Inuktitut (Latin), Canada |
| 632 | iu_cans = 0x785D, // Inuktitut (Syllabics) |
| 633 | iu_cans_ca = 0x045d, // Inuktitut (Syllabics), Canada |
| 634 | ga = 0x003C, // Irish |
| 635 | ga_ie = 0x083C, // Irish, Ireland |
| 636 | it = 0x0010, // Italian |
| 637 | it_it = 0x0410, // Italian, Italy |
| 638 | it_ch = 0x0810, // Italian, Switzerland |
| 639 | ja = 0x0011, // Japanese |
| 640 | ja_jp = 0x0411, // Japanese, Japan |
| 641 | kn = 0x004B, // Kannada |
| 642 | kn_in = 0x044B, // Kannada, India |
| 643 | kr_latn_ng = 0x0471, // Kanuri (Latin), Nigeria |
| 644 | ks = 0x0060, // Kashmiri |
| 645 | ks_arab = 0x0460, // Kashmiri, Perso-Arabic |
| 646 | ks_deva_in = 0x0860, // Kashmiri (Devanagari), India |
| 647 | kk = 0x003F, // Kazakh |
| 648 | kk_kz = 0x043F, // Kazakh, Kazakhstan |
| 649 | km = 0x0053, // Khmer |
| 650 | km_kh = 0x0453, // Khmer, Cambodia |
| 651 | quc = 0x0086, // K'iche |
| 652 | quc_latn_gt = 0x0486, // K'iche, Guatemala |
| 653 | rw = 0x0087, // Kinyarwanda |
| 654 | rw_rw = 0x0487, // Kinyarwanda, Rwanda |
| 655 | sw = 0x0041, // Kiswahili |
| 656 | sw_ke = 0x0441, // Kiswahili, Kenya |
| 657 | kok = 0x0057, // Konkani |
| 658 | kok_in = 0x0457, // Konkani, India |
| 659 | ko = 0x0012, // Korean |
| 660 | ko_kr = 0x0412, // Korean, Korea |
| 661 | ky = 0x0040, // Kyrgyz |
| 662 | ky_kg = 0x0440, // Kyrgyz, Kyrgyzstan |
| 663 | lo = 0x0054, // Lao |
| 664 | lo_la = 0x0454, // Lao, Lao P.D.R. |
| 665 | la_va = 0x0476, // Latin, Vatican City |
| 666 | lv = 0x0026, // Latvian |
| 667 | lv_lv = 0x0426, // Latvian, Latvia |
| 668 | lt = 0x0027, // Lithuanian |
| 669 | lt_lt = 0x0427, // Lithuanian, Lithuania |
| 670 | dsb = 0x7C2E, // Lower Sorbian |
| 671 | dsb_de = 0x082E, // Lower Sorbian, Germany |
| 672 | lb = 0x006E, // Luxembourgish |
| 673 | lb_lu = 0x046E, // Luxembourgish, Luxembourg |
| 674 | mk = 0x002F, // Macedonian |
| 675 | mk_mk = 0x042F, // Macedonian, North Macedonia |
| 676 | ms = 0x003E, // Malay |
| 677 | ms_bn = 0x083E, // Malay, Brunei Darussalam |
| 678 | ms_my = 0x043E, // Malay, Malaysia |
| 679 | ml = 0x004C, // Malayalam |
| 680 | ml_in = 0x044C, // Malayalam, India |
| 681 | mt = 0x003A, // Maltese |
| 682 | mt_mt = 0x043A, // Maltese, Malta |
| 683 | mi = 0x0081, // Maori |
| 684 | mi_nz = 0x0481, // Maori, New Zealand |
| 685 | arn = 0x007A, // Mapudungun |
| 686 | arn_cl = 0x047A, // Mapudungun, Chile |
| 687 | mr = 0x004E, // Marathi |
| 688 | mr_in = 0x044E, // Marathi, India |
| 689 | moh = 0x007C, // Mohawk |
| 690 | moh_ca = 0x047C, // Mohawk, Canada |
| 691 | mn = 0x0050, // Mongolian (Cyrillic) |
| 692 | mn_cyrl = 0x7850, // Mongolian (Cyrillic) |
| 693 | mn_mn = 0x0450, // Mongolian (Cyrillic), Mongolia |
| 694 | mn_mong = 0x7C50, // Mongolian (Traditional Mongolian) |
| 695 | mn_mong_cn = 0x0850, // Mongolian (Traditional Mongolian), People's Republic of China |
| 696 | mn_mong_mn = 0x0C50, // Mongolian (Traditional Mongolian), Mongolia |
| 697 | ne = 0x0061, // Nepali |
| 698 | ne_in = 0x0861, // Nepali, India |
| 699 | ne_np = 0x0461, // Nepali, Nepal |
| 700 | no = 0x0014, // Norwegian (Bokmal) |
| 701 | nb = 0x7C14, // Norwegian (Bokmal) |
| 702 | nb_no = 0x0414, // Norwegian (Bokmal), Norway |
| 703 | nn = 0x7814, // Norwegian (Nynorsk) |
| 704 | nn_no = 0x0814, // Norwegian (Nynorsk), Norway |
| 705 | oc = 0x0082, // Occitan |
| 706 | oc_fr = 0x0482, // Occitan, France |
| 707 | @"or" = 0x0048, // Odia |
| 708 | or_in = 0x0448, // Odia, India |
| 709 | om = 0x0072, // Oromo |
| 710 | om_et = 0x0472, // Oromo, Ethiopia |
| 711 | ps = 0x0063, // Pashto |
| 712 | ps_af = 0x0463, // Pashto, Afghanistan |
| 713 | fa = 0x0029, // Persian |
| 714 | fa_ir = 0x0429, // Persian, Iran |
| 715 | pl = 0x0015, // Polish |
| 716 | pl_pl = 0x0415, // Polish, Poland |
| 717 | pt = 0x0016, // Portuguese |
| 718 | pt_br = 0x0416, // Portuguese, Brazil |
| 719 | pt_pt = 0x0816, // Portuguese, Portugal |
| 720 | qps_ploca = 0x05FE, // Pseudo Language, Pseudo locale for east Asian/complex script localization testing |
| 721 | qps_ploc = 0x0501, // Pseudo Language, Pseudo locale used for localization testing |
| 722 | qps_plocm = 0x09FF, // Pseudo Language, Pseudo locale used for localization testing of mirrored locales |
| 723 | pa = 0x0046, // Punjabi |
| 724 | pa_arab = 0x7C46, // Punjabi |
| 725 | pa_in = 0x0446, // Punjabi, India |
| 726 | pa_arab_pk = 0x0846, // Punjabi, Islamic Republic of Pakistan |
| 727 | quz = 0x006B, // Quechua |
| 728 | quz_bo = 0x046B, // Quechua, Bolivia |
| 729 | quz_ec = 0x086B, // Quechua, Ecuador |
| 730 | quz_pe = 0x0C6B, // Quechua, Peru |
| 731 | ro = 0x0018, // Romanian |
| 732 | ro_md = 0x0818, // Romanian, Moldova |
| 733 | ro_ro = 0x0418, // Romanian, Romania |
| 734 | rm = 0x0017, // Romansh |
| 735 | rm_ch = 0x0417, // Romansh, Switzerland |
| 736 | ru = 0x0019, // Russian |
| 737 | ru_md = 0x0819, // Russian, Moldova |
| 738 | ru_ru = 0x0419, // Russian, Russia |
| 739 | sah = 0x0085, // Sakha |
| 740 | sah_ru = 0x0485, // Sakha, Russia |
| 741 | smn = 0x703B, // Sami (Inari) |
| 742 | smn_fi = 0x243B, // Sami (Inari), Finland |
| 743 | smj = 0x7C3B, // Sami (Lule) |
| 744 | smj_no = 0x103B, // Sami (Lule), Norway |
| 745 | smj_se = 0x143B, // Sami (Lule), Sweden |
| 746 | se = 0x003B, // Sami (Northern) |
| 747 | se_fi = 0x0C3B, // Sami (Northern), Finland |
| 748 | se_no = 0x043B, // Sami (Northern), Norway |
| 749 | se_se = 0x083B, // Sami (Northern), Sweden |
| 750 | sms = 0x743B, // Sami (Skolt) |
| 751 | sms_fi = 0x203B, // Sami (Skolt), Finland |
| 752 | sma = 0x783B, // Sami (Southern) |
| 753 | sma_no = 0x183B, // Sami (Southern), Norway |
| 754 | sma_se = 0x1C3B, // Sami (Southern), Sweden |
| 755 | sa = 0x004F, // Sanskrit |
| 756 | sa_in = 0x044F, // Sanskrit, India |
| 757 | gd = 0x0091, // Scottish Gaelic |
| 758 | gd_gb = 0x0491, // Scottish Gaelic, United Kingdom |
| 759 | sr_cyrl = 0x6C1A, // Serbian (Cyrillic) |
| 760 | sr_cyrl_ba = 0x1C1A, // Serbian (Cyrillic), Bosnia and Herzegovina |
| 761 | sr_cyrl_me = 0x301A, // Serbian (Cyrillic), Montenegro |
| 762 | sr_cyrl_rs = 0x281A, // Serbian (Cyrillic), Serbia |
| 763 | sr_cyrl_cs = 0x0C1A, // Serbian (Cyrillic), Serbia and Montenegro (Former) |
| 764 | sr_latn = 0x701A, // Serbian (Latin) |
| 765 | sr = 0x7C1A, // Serbian (Latin) |
| 766 | sr_latn_ba = 0x181A, // Serbian (Latin), Bosnia and Herzegovina |
| 767 | sr_latn_me = 0x2c1A, // Serbian (Latin), Montenegro |
| 768 | sr_latn_rs = 0x241A, // Serbian (Latin), Serbia |
| 769 | sr_latn_cs = 0x081A, // Serbian (Latin), Serbia and Montenegro (Former) |
| 770 | nso = 0x006C, // Sesotho sa Leboa |
| 771 | nso_za = 0x046C, // Sesotho sa Leboa, South Africa |
| 772 | tn = 0x0032, // Setswana |
| 773 | tn_bw = 0x0832, // Setswana, Botswana |
| 774 | tn_za = 0x0432, // Setswana, South Africa |
| 775 | sd = 0x0059, // Sindhi |
| 776 | sd_arab = 0x7C59, // Sindhi |
| 777 | sd_arab_pk = 0x0859, // Sindhi, Islamic Republic of Pakistan |
| 778 | si = 0x005B, // Sinhala |
| 779 | si_lk = 0x045B, // Sinhala, Sri Lanka |
| 780 | sk = 0x001B, // Slovak |
| 781 | sk_sk = 0x041B, // Slovak, Slovakia |
| 782 | sl = 0x0024, // Slovenian |
| 783 | sl_si = 0x0424, // Slovenian, Slovenia |
| 784 | so = 0x0077, // Somali |
| 785 | so_so = 0x0477, // Somali, Somalia |
| 786 | st = 0x0030, // Sotho |
| 787 | st_za = 0x0430, // Sotho, South Africa |
| 788 | es = 0x000A, // Spanish |
| 789 | es_ar = 0x2C0A, // Spanish, Argentina |
| 790 | es_ve = 0x200A, // Spanish, Bolivarian Republic of Venezuela |
| 791 | es_bo = 0x400A, // Spanish, Bolivia |
| 792 | es_cl = 0x340A, // Spanish, Chile |
| 793 | es_co = 0x240A, // Spanish, Colombia |
| 794 | es_cr = 0x140A, // Spanish, Costa Rica |
| 795 | es_cu = 0x5c0A, // Spanish, Cuba |
| 796 | es_do = 0x1c0A, // Spanish, Dominican Republic |
| 797 | es_ec = 0x300A, // Spanish, Ecuador |
| 798 | es_sv = 0x440A, // Spanish, El Salvador |
| 799 | es_gt = 0x100A, // Spanish, Guatemala |
| 800 | es_hn = 0x480A, // Spanish, Honduras |
| 801 | es_419 = 0x580A, // Spanish, Latin America |
| 802 | es_mx = 0x080A, // Spanish, Mexico |
| 803 | es_ni = 0x4C0A, // Spanish, Nicaragua |
| 804 | es_pa = 0x180A, // Spanish, Panama |
| 805 | es_py = 0x3C0A, // Spanish, Paraguay |
| 806 | es_pe = 0x280A, // Spanish, Peru |
| 807 | es_pr = 0x500A, // Spanish, Puerto Rico |
| 808 | es_es_tradnl = 0x040A, // Spanish, Spain |
| 809 | es_es = 0x0c0A, // Spanish, Spain |
| 810 | es_us = 0x540A, // Spanish, United States |
| 811 | es_uy = 0x380A, // Spanish, Uruguay |
| 812 | sv = 0x001D, // Swedish |
| 813 | sv_fi = 0x081D, // Swedish, Finland |
| 814 | sv_se = 0x041D, // Swedish, Sweden |
| 815 | syr = 0x005A, // Syriac |
| 816 | syr_sy = 0x045A, // Syriac, Syria |
| 817 | tg = 0x0028, // Tajik (Cyrillic) |
| 818 | tg_cyrl = 0x7C28, // Tajik (Cyrillic) |
| 819 | tg_cyrl_tj = 0x0428, // Tajik (Cyrillic), Tajikistan |
| 820 | tzm = 0x005F, // Tamazight (Latin) |
| 821 | tzm_latn = 0x7C5F, // Tamazight (Latin) |
| 822 | tzm_latn_dz = 0x085F, // Tamazight (Latin), Algeria |
| 823 | ta = 0x0049, // Tamil |
| 824 | ta_in = 0x0449, // Tamil, India |
| 825 | ta_lk = 0x0849, // Tamil, Sri Lanka |
| 826 | tt = 0x0044, // Tatar |
| 827 | tt_ru = 0x0444, // Tatar, Russia |
| 828 | te = 0x004A, // Telugu |
| 829 | te_in = 0x044A, // Telugu, India |
| 830 | th = 0x001E, // Thai |
| 831 | th_th = 0x041E, // Thai, Thailand |
| 832 | bo = 0x0051, // Tibetan |
| 833 | bo_cn = 0x0451, // Tibetan, People's Republic of China |
| 834 | ti = 0x0073, // Tigrinya |
| 835 | ti_er = 0x0873, // Tigrinya, Eritrea |
| 836 | ti_et = 0x0473, // Tigrinya, Ethiopia |
| 837 | ts = 0x0031, // Tsonga |
| 838 | ts_za = 0x0431, // Tsonga, South Africa |
| 839 | tr = 0x001F, // Turkish |
| 840 | tr_tr = 0x041F, // Turkish, Turkey |
| 841 | tk = 0x0042, // Turkmen |
| 842 | tk_tm = 0x0442, // Turkmen, Turkmenistan |
| 843 | uk = 0x0022, // Ukrainian |
| 844 | uk_ua = 0x0422, // Ukrainian, Ukraine |
| 845 | hsb = 0x002E, // Upper Sorbian |
| 846 | hsb_de = 0x042E, // Upper Sorbian, Germany |
| 847 | ur = 0x0020, // Urdu |
| 848 | ur_in = 0x0820, // Urdu, India |
| 849 | ur_pk = 0x0420, // Urdu, Islamic Republic of Pakistan |
| 850 | ug = 0x0080, // Uyghur |
| 851 | ug_cn = 0x0480, // Uyghur, People's Republic of China |
| 852 | uz_cyrl = 0x7843, // Uzbek (Cyrillic) |
| 853 | uz_cyrl_uz = 0x0843, // Uzbek (Cyrillic), Uzbekistan |
| 854 | uz = 0x0043, // Uzbek (Latin) |
| 855 | uz_latn = 0x7C43, // Uzbek (Latin) |
| 856 | uz_latn_uz = 0x0443, // Uzbek (Latin), Uzbekistan |
| 857 | ca_es_valencia = 0x0803, // Valencian, Spain |
| 858 | ve = 0x0033, // Venda |
| 859 | ve_za = 0x0433, // Venda, South Africa |
| 860 | vi = 0x002A, // Vietnamese |
| 861 | vi_vn = 0x042A, // Vietnamese, Vietnam |
| 862 | cy = 0x0052, // Welsh |
| 863 | cy_gb = 0x0452, // Welsh, United Kingdom |
| 864 | wo = 0x0088, // Wolof |
| 865 | wo_sn = 0x0488, // Wolof, Senegal |
| 866 | xh = 0x0034, // Xhosa |
| 867 | xh_za = 0x0434, // Xhosa, South Africa |
| 868 | ii = 0x0078, // Yi |
| 869 | ii_cn = 0x0478, // Yi, People's Republic of China |
| 870 | yi_001 = 0x043D, // Yiddish, World |
| 871 | yo = 0x006A, // Yoruba |
| 872 | yo_ng = 0x046A, // Yoruba, Nigeria |
| 873 | zu = 0x0035, // Zulu |
| 874 | zu_za = 0x0435, // Zulu, South Africa |
| 875 | |
| 876 | /// Special case |
| 877 | x_iv_mathan = 0x007F, // LANG_INVARIANT, "math alphanumeric sorting" |
| 878 | }; |