Language tag format expressed as a regular expression (rough approximation):
[a-zA-Z]{1,3}([-_][a-zA-Z]{4})?([-_][a-zA-Z]{2})?([-_][a-zA-Z0-9]{1,8})? lang | script | country | suffix
Notes:
a-0 and a-00000000 are allowed).en-us-blah-blah1-blah2-blah3 is allowed.ca-es-valencia is not considered equivalent to ca-es-valencia-blah.qps and the suffix is ploca or plocmpub fn parse(lang_tag: []const u8) error
pub fn parse(lang_tag: []const u8) error{InvalidLanguageTag}!Parsed {
var it = std.mem.splitAny(u8, lang_tag, "-_");
const lang_code = it.first();
const is_valid_lang_code = lang_code.len >= 1 and lang_code.len <= 3 and isAllAlphabetic(lang_code);
if (!is_valid_lang_code) return error.InvalidLanguageTag;
var parsed = Parsed{
.language_code = lang_code,
};
// The second part could be a script tag, a country code, or a suffix
if (it.next()) |part_str| {
// The lang code being length 1 behaves strangely, so fully special case it.
if (lang_code.len == 1) {
// This is almost certainly not the 'right' way to do this, but I don't have a method
// to determine how exactly these language tags are parsed, and it seems like
// suffix parsing rules apply generally (digits allowed, length of 1 to 8).
//
// However, because we want to be able to lookup `x-iv-mathan` normally without
// `multiple_suffixes` being set to true, we need to make sure to treat two-length
// alphabetic parts as a country code.
if (part_str.len == 2 and isAllAlphabetic(part_str)) {
parsed.country_code = part_str;
}
// Everything else, though, we can just throw into the suffix as long as the normal
// rules apply.
else if (part_str.len > 0 and part_str.len <= 8 and isAllAlphanumeric(part_str)) {
parsed.suffix = part_str;
} else {
return error.InvalidLanguageTag;
}
} else if (part_str.len == 4 and isAllAlphabetic(part_str)) {
parsed.script_tag = part_str;
} else if (part_str.len == 2 and isAllAlphabetic(part_str)) {
parsed.country_code = part_str;
}
// Only a 3-len numeric suffix is allowed as the second part of a tag
else if (part_str.len == 3 and isAllNumeric(part_str)) {
parsed.suffix = part_str;
}
// Special case for qps-ploca and qps-plocm
else if (std.ascii.eqlIgnoreCase(lang_code, "qps") and
(std.ascii.eqlIgnoreCase(part_str, "ploca") or
std.ascii.eqlIgnoreCase(part_str, "plocm")))
{
parsed.suffix = part_str;
} else {
return error.InvalidLanguageTag;
}
} else {
// If there's no part besides a 1-len lang code, then it is malformed
if (lang_code.len == 1) return error.InvalidLanguageTag;
return parsed;
}
if (parsed.script_tag != null) {
if (it.next()) |part_str| {
if (part_str.len == 2 and isAllAlphabetic(part_str)) {
parsed.country_code = part_str;
} else {
// Suffix is not allowed when a country code is not present.
return error.InvalidLanguageTag;
}
} else {
return parsed;
}
}
// We've now parsed any potential script tag/country codes, so anything remaining
// is a suffix
while (it.next()) |part_str| {
if (part_str.len == 0 or part_str.len > 8 or !isAllAlphanumeric(part_str)) {
return error.InvalidLanguageTag;
}
if (parsed.suffix == null) {
parsed.suffix = part_str;
} else {
// In theory we could return early here but we still want to validate
// that each part is a valid suffix all the way to the end, e.g.
// we should reject `en-us-suffix-a-b-c-!!!` because of the invalid `!!!`
// suffix part.
parsed.multiple_suffixes = true;
}
}
return parsed;
}