private static string GetImplicitScriptCode(LanguageSubtag languageSubtag, RegionSubtag regionSubtag) { if (languageSubtag == null || languageSubtag.IsPrivateUse) return null; string regionCode = null; if (regionSubtag != null && !regionSubtag.IsPrivateUse) regionCode = regionSubtag; return GetImplicitScriptCode(languageSubtag.Code, regionCode); }
/// <summary> /// Gets the subtags of the specified language tag. /// </summary> /// <param name="langTag">The language tag.</param> /// <param name="languageSubtag">The language subtag.</param> /// <param name="scriptSubtag">The script subtag.</param> /// <param name="regionSubtag">The region subtag.</param> /// <param name="variantSubtags">The variant and private-use subtags.</param> /// <returns></returns> public static bool TryGetSubtags(string langTag, out LanguageSubtag languageSubtag, out ScriptSubtag scriptSubtag, out RegionSubtag regionSubtag, out IEnumerable<VariantSubtag> variantSubtags) { if (langTag == null) throw new ArgumentNullException("langTag"); languageSubtag = null; scriptSubtag = null; regionSubtag = null; variantSubtags = null; Match match = LangTagPattern.Match(langTag); if (!match.Success) return false; var privateUseCodes = new List<string>(); Group privateUseGroup = match.Groups["privateuse"]; if (privateUseGroup.Success) privateUseCodes.AddRange(privateUseGroup.Value.Split(new[] {'-'}, StringSplitOptions.RemoveEmptyEntries).Skip(1)); Group languageGroup = match.Groups["language"]; if (languageGroup.Success) { string languageCode = languageGroup.Value; if (languageCode.Equals(WellKnownSubtags.UnlistedLanguage, StringComparison.OrdinalIgnoreCase)) { // In our own WS dialog, we don't allow no language, but if it isn't a standard one, a language like xkal // produces an identifier like qaa-x-kal, and we interepret the first thing after the x as a private // language code (not allowed as the first three characters according to the standard). // If it's NOT a valid language code (e.g., too many characters), probably came from some other // program. Treating it as a language code will fail if we try to create such a writing system, // since we will detect the invalid language code. So only interpret the first element // after the x as a language code if it is a valid one. Otherwise, we just let qaa be the language. if (privateUseCodes.Count > 0 && LangPattern.IsMatch(privateUseCodes[0]) && !StandardSubtags.CommonPrivateUseVariants.Contains(privateUseCodes[0])) { languageSubtag = new LanguageSubtag(privateUseCodes[0]); privateUseCodes.RemoveAt(0); } else { languageSubtag = WellKnownSubtags.UnlistedLanguage; // We do allow just plain qaa. } } else { if (!StandardSubtags.IsValidIso639LanguageCode(languageCode)) return false; languageSubtag = languageCode; } } Group scriptGroup = match.Groups["script"]; if (scriptGroup.Success) { string scriptCode = scriptGroup.Value; if (scriptCode.Equals("Qaaa", StringComparison.OrdinalIgnoreCase) && privateUseCodes.Count > 0 && ScriptPattern.IsMatch(privateUseCodes[0])) { scriptSubtag = new ScriptSubtag(privateUseCodes[0]); privateUseCodes.RemoveAt(0); } else { if (!StandardSubtags.IsValidIso15924ScriptCode(scriptCode)) return false; scriptSubtag = scriptCode; } } Group regionGroup = match.Groups["region"]; if (regionGroup.Success) { string regionCode = regionGroup.Value; if (regionCode.Equals("QM", StringComparison.OrdinalIgnoreCase) && privateUseCodes.Count > 0 && RegionPattern.IsMatch(privateUseCodes[0])) { regionSubtag = new RegionSubtag(privateUseCodes[0]); privateUseCodes.RemoveAt(0); } else { if (!StandardSubtags.IsValidIso3166RegionCode(regionCode)) return false; regionSubtag = regionCode; } } if (scriptSubtag == null) scriptSubtag = GetImplicitScriptCode(languageSubtag, regionSubtag); var variants = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); var variantSubtagsList = new List<VariantSubtag>(); Group variantGroup = match.Groups["variant"]; if (variantGroup.Success) { foreach (string variantCode in variantGroup.Value.Split(new[] {'-'}, StringSplitOptions.RemoveEmptyEntries)) { if (variants.Contains(variantCode)) return false; VariantSubtag variantSubtag; if (!StandardSubtags.RegisteredVariants.TryGet(variantCode, out variantSubtag)) return false; variantSubtagsList.Add(variantSubtag); variants.Add(variantCode); } } variants.Clear(); foreach (string privateUseCode in privateUseCodes) { if (variants.Contains(privateUseCode) || privateUseCode.Equals("x", StringComparison.InvariantCultureIgnoreCase)) return false; VariantSubtag variantSubtag; if (!StandardSubtags.CommonPrivateUseVariants.TryGet(privateUseCode, out variantSubtag)) variantSubtag = new VariantSubtag(privateUseCode); variantSubtagsList.Add(variantSubtag); variants.Add(privateUseCode); } variantSubtags = variantSubtagsList; return true; }
private static bool TryCreate(LanguageSubtag languageSubtag, ScriptSubtag scriptSubtag, RegionSubtag regionSubtag, IEnumerable<VariantSubtag> variantSubtags, out string langTag, out string message, out string paramName) { message = null; paramName = null; VariantSubtag[] variantSubtagsArray = variantSubtags.ToArray(); if (languageSubtag == null && (scriptSubtag != null || regionSubtag != null || variantSubtagsArray.Length == 0 || variantSubtagsArray.Any(v => !v.IsPrivateUse))) { message = "A language subtag is required."; paramName = "languageSubtag"; } var sb = new StringBuilder(); bool isCustomLanguage = false; if (languageSubtag != null) { // Insert non-custom language, script, region into main part of code. if (languageSubtag.IsPrivateUse && languageSubtag.Code != WellKnownSubtags.UnlistedLanguage) { if (!LangPattern.IsMatch(languageSubtag.Code)) { message = "The private use language code is invalid."; paramName = "languageSubtag"; } sb.Append("qaa"); isCustomLanguage = true; } else { sb.Append(languageSubtag.Code); } } bool isCustomScript = false; if (scriptSubtag != null && GetImplicitScriptCode(languageSubtag, regionSubtag) != scriptSubtag.Code) { if (sb.Length > 0) sb.Append("-"); // Qaaa is our flag to expect a script in private-use. If the actual value is Qaaa, we need to treat it as custom, // so we don't confuse some other private-use tag with a custom script. if (scriptSubtag.IsPrivateUse && !StandardSubtags.IsPrivateUseScriptCode(scriptSubtag.Code)) { if (message == null && !ScriptPattern.IsMatch(scriptSubtag.Code)) { message = "The private use script code is invalid."; paramName = "scriptSubtag"; } sb.Append("Qaaa"); isCustomScript = true; } else { sb.Append(scriptSubtag.Code); } } bool isCustomRegion = false; if (regionSubtag != null) { if (sb.Length > 0) sb.Append("-"); // QM is our flag to expect a region in private-use. If the actual value is QM, we need to treat it as custom, // so we don't confuse some other private-use tag with a custom region. if (regionSubtag.IsPrivateUse && !StandardSubtags.IsPrivateUseRegionCode(regionSubtag.Code)) { if (message == null && !RegionPattern.IsMatch(regionSubtag.Code)) { message = "The private use region code is invalid."; paramName = "regionSubtag"; } sb.Append("QM"); isCustomRegion = true; } else { sb.Append(regionSubtag.Code); } } var variants = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); foreach (VariantSubtag variantSubtag in variantSubtagsArray.Where(vs => !vs.IsPrivateUse)) { if (message == null && variants.Contains(variantSubtag.Code)) { message = "Duplicate variants are not allowed."; paramName = "variantSubtags"; } if (sb.Length > 0) sb.Append("-"); sb.Append(variantSubtag.Code); variants.Add(variantSubtag.Code); } // Insert custom language, script, or variant into private=use. bool inPrivateUse = false; if (isCustomLanguage) { inPrivateUse = true; if (sb.Length > 0) sb.Append("-"); sb.Append("x-"); sb.Append(languageSubtag.Code); } if (isCustomScript) { if (sb.Length > 0) sb.Append("-"); if (!inPrivateUse) { inPrivateUse = true; sb.Append("x-"); } sb.Append(scriptSubtag.Code); } if (isCustomRegion) { if (sb.Length > 0) sb.Append("-"); if (!inPrivateUse) { inPrivateUse = true; sb.Append("x-"); } sb.Append(regionSubtag.Code); } variants.Clear(); foreach (VariantSubtag variantSubtag in variantSubtagsArray.Where(vs => vs.IsPrivateUse)) { if (message == null && !PrivateUsePattern.IsMatch(variantSubtag.Code)) { message = "The private use subtags contains an invalid subtag."; paramName = "variantSubtags"; } if (message == null && variants.Contains(variantSubtag.Code)) { message = "Duplicate private use subtags are not allowed."; paramName = "variantSubtags"; } if (sb.Length > 0) sb.Append("-"); if (!inPrivateUse) { inPrivateUse = true; sb.Append("x-"); } sb.Append(variantSubtag.Code); variants.Add(variantSubtag.Code); } langTag = sb.ToString(); return message == null; }
/// <summary> /// Validates the creation of an IETF language tag. /// </summary> public static bool Validate(LanguageSubtag languageSubtag, ScriptSubtag scriptSubtag, RegionSubtag regionSubtag, IEnumerable<VariantSubtag> variantSubtags, out string message) { string langTag, paramName; return TryCreate(languageSubtag, scriptSubtag, regionSubtag, variantSubtags, out langTag, out message, out paramName); }
/// <summary> /// Creates a language tag from the specified subtags. /// </summary> public static string Create(LanguageSubtag languageSubtag, ScriptSubtag scriptSubtag, RegionSubtag regionSubtag, IEnumerable<VariantSubtag> variantSubtags, bool validate = true) { string langTag, message, paramName; if (!TryCreate(languageSubtag, scriptSubtag, regionSubtag, variantSubtags, out langTag, out message, out paramName)) { if (validate) throw new ArgumentException(message, paramName); } return langTag; }
/// <summary> /// Tries to create a language tag from the specified subtags. /// </summary> public static bool TryCreate(LanguageSubtag languageSubtag, ScriptSubtag scriptSubtag, RegionSubtag regionSubtag, IEnumerable<VariantSubtag> variantSubtags, out string langTag) { string message, paramName; if (!TryCreate(languageSubtag, scriptSubtag, regionSubtag, variantSubtags, out langTag, out message, out paramName)) { langTag = null; return false; } return true; }