| | 1 | | using System; |
| | 2 | | using System.Collections.Concurrent; |
| | 3 | | using System.Collections.Frozen; |
| | 4 | | using System.Collections.Generic; |
| | 5 | | using System.Diagnostics.CodeAnalysis; |
| | 6 | | using System.IO; |
| | 7 | | using System.Linq; |
| | 8 | | using System.Reflection; |
| | 9 | | using System.Text.Json; |
| | 10 | | using System.Threading.Tasks; |
| | 11 | | using Jellyfin.Extensions; |
| | 12 | | using Jellyfin.Extensions.Json; |
| | 13 | | using MediaBrowser.Controller.Configuration; |
| | 14 | | using MediaBrowser.Model.Entities; |
| | 15 | | using MediaBrowser.Model.Globalization; |
| | 16 | | using Microsoft.Extensions.Logging; |
| | 17 | |
|
| | 18 | | namespace Emby.Server.Implementations.Localization |
| | 19 | | { |
| | 20 | | /// <summary> |
| | 21 | | /// Class LocalizationManager. |
| | 22 | | /// </summary> |
| | 23 | | public class LocalizationManager : ILocalizationManager |
| | 24 | | { |
| | 25 | | private const string DefaultCulture = "en-US"; |
| | 26 | | private const string RatingsPath = "Emby.Server.Implementations.Localization.Ratings."; |
| | 27 | | private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt"; |
| | 28 | | private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json"; |
| 2 | 29 | | private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; |
| 2 | 30 | | private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"]; |
| | 31 | |
|
| | 32 | | private readonly IServerConfigurationManager _configurationManager; |
| | 33 | | private readonly ILogger<LocalizationManager> _logger; |
| | 34 | |
|
| 53 | 35 | | private readonly Dictionary<string, Dictionary<string, ParentalRatingScore?>> _allParentalRatings = new(StringCo |
| | 36 | |
|
| 53 | 37 | | private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = new(StringComparer.Ord |
| | 38 | |
|
| 53 | 39 | | private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; |
| | 40 | |
|
| 53 | 41 | | private List<CultureDto> _cultures = []; |
| | 42 | |
|
| | 43 | | private FrozenDictionary<string, string> _iso6392BtoT = null!; |
| | 44 | |
|
| | 45 | | /// <summary> |
| | 46 | | /// Initializes a new instance of the <see cref="LocalizationManager" /> class. |
| | 47 | | /// </summary> |
| | 48 | | /// <param name="configurationManager">The configuration manager.</param> |
| | 49 | | /// <param name="logger">The logger.</param> |
| | 50 | | public LocalizationManager( |
| | 51 | | IServerConfigurationManager configurationManager, |
| | 52 | | ILogger<LocalizationManager> logger) |
| | 53 | | { |
| 53 | 54 | | _configurationManager = configurationManager; |
| 53 | 55 | | _logger = logger; |
| 53 | 56 | | } |
| | 57 | |
|
| | 58 | | /// <summary> |
| | 59 | | /// Loads all resources into memory. |
| | 60 | | /// </summary> |
| | 61 | | /// <returns><see cref="Task" />.</returns> |
| | 62 | | public async Task LoadAll() |
| | 63 | | { |
| | 64 | | // Extract from the assembly |
| | 65 | | foreach (var resource in _assembly.GetManifestResourceNames()) |
| | 66 | | { |
| | 67 | | if (!resource.StartsWith(RatingsPath, StringComparison.Ordinal)) |
| | 68 | | { |
| | 69 | | continue; |
| | 70 | | } |
| | 71 | |
|
| | 72 | | using var stream = _assembly.GetManifestResourceStream(resource); |
| | 73 | | if (stream is not null) |
| | 74 | | { |
| | 75 | | var ratingSystem = await JsonSerializer.DeserializeAsync<ParentalRatingSystem>(stream, _jsonOptions) |
| | 76 | | ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); |
| | 77 | |
|
| | 78 | | var dict = new Dictionary<string, ParentalRatingScore?>(); |
| | 79 | | if (ratingSystem.Ratings is not null) |
| | 80 | | { |
| | 81 | | foreach (var ratingEntry in ratingSystem.Ratings) |
| | 82 | | { |
| | 83 | | foreach (var ratingString in ratingEntry.RatingStrings) |
| | 84 | | { |
| | 85 | | dict[ratingString] = ratingEntry.RatingScore; |
| | 86 | | } |
| | 87 | | } |
| | 88 | |
|
| | 89 | | _allParentalRatings[ratingSystem.CountryCode] = dict; |
| | 90 | | } |
| | 91 | | } |
| | 92 | | } |
| | 93 | |
|
| | 94 | | await LoadCultures().ConfigureAwait(false); |
| | 95 | | } |
| | 96 | |
|
| | 97 | | /// <summary> |
| | 98 | | /// Gets the cultures. |
| | 99 | | /// </summary> |
| | 100 | | /// <returns><see cref="IEnumerable{CultureDto}" />.</returns> |
| | 101 | | public IEnumerable<CultureDto> GetCultures() |
| 1 | 102 | | => _cultures; |
| | 103 | |
|
| | 104 | | private async Task LoadCultures() |
| | 105 | | { |
| | 106 | | List<CultureDto> list = []; |
| | 107 | | Dictionary<string, string> iso6392BtoTdict = new Dictionary<string, string>(); |
| | 108 | |
|
| | 109 | | using var stream = _assembly.GetManifestResourceStream(CulturesPath); |
| | 110 | | if (stream is null) |
| | 111 | | { |
| | 112 | | throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); |
| | 113 | | } |
| | 114 | | else |
| | 115 | | { |
| | 116 | | using var reader = new StreamReader(stream); |
| | 117 | | await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) |
| | 118 | | { |
| | 119 | | if (string.IsNullOrWhiteSpace(line)) |
| | 120 | | { |
| | 121 | | continue; |
| | 122 | | } |
| | 123 | |
|
| | 124 | | var parts = line.Split('|'); |
| | 125 | | if (parts.Length != 5) |
| | 126 | | { |
| | 127 | | throw new InvalidDataException($"Invalid culture data found at: '{line}'"); |
| | 128 | | } |
| | 129 | |
|
| | 130 | | string name = parts[3]; |
| | 131 | | if (string.IsNullOrWhiteSpace(name)) |
| | 132 | | { |
| | 133 | | continue; |
| | 134 | | } |
| | 135 | |
|
| | 136 | | string twoCharName = parts[2]; |
| | 137 | | if (string.IsNullOrWhiteSpace(twoCharName)) |
| | 138 | | { |
| | 139 | | continue; |
| | 140 | | } |
| | 141 | |
|
| | 142 | | string[] threeLetterNames; |
| | 143 | | if (string.IsNullOrWhiteSpace(parts[1])) |
| | 144 | | { |
| | 145 | | threeLetterNames = [parts[0]]; |
| | 146 | | } |
| | 147 | | else |
| | 148 | | { |
| | 149 | | threeLetterNames = [parts[0], parts[1]]; |
| | 150 | |
|
| | 151 | | // In cases where there are two TLN the first one is ISO 639-2/T and the second one is ISO 639-2 |
| | 152 | | // We need ISO 639-2/T for the .NET cultures so we cultivate a dictionary for the translation B- |
| | 153 | | iso6392BtoTdict.TryAdd(parts[1], parts[0]); |
| | 154 | | } |
| | 155 | |
|
| | 156 | | list.Add(new CultureDto(name, name, twoCharName, threeLetterNames)); |
| | 157 | | } |
| | 158 | |
|
| | 159 | | _cultures = list; |
| | 160 | | _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); |
| | 161 | | } |
| | 162 | | } |
| | 163 | |
|
| | 164 | | /// <inheritdoc /> |
| | 165 | | public CultureDto? FindLanguageInfo(string language) |
| | 166 | | { |
| | 167 | | // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs |
| 702 | 168 | | for (var i = 0; i < _cultures.Count; i++) |
| | 169 | | { |
| 350 | 170 | | var culture = _cultures[i]; |
| 350 | 171 | | if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) |
| 350 | 172 | | || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) |
| 350 | 173 | | || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase) |
| 350 | 174 | | || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) |
| | 175 | | { |
| 5 | 176 | | return culture; |
| | 177 | | } |
| | 178 | | } |
| | 179 | |
|
| 1 | 180 | | return default; |
| | 181 | | } |
| | 182 | |
|
| | 183 | | /// <inheritdoc /> |
| | 184 | | public IReadOnlyList<CountryInfo> GetCountries() |
| | 185 | | { |
| 1 | 186 | | using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException |
| | 187 | |
|
| 1 | 188 | | return JsonSerializer.Deserialize<IReadOnlyList<CountryInfo>>(stream, _jsonOptions) ?? []; |
| 1 | 189 | | } |
| | 190 | |
|
| | 191 | | /// <inheritdoc /> |
| | 192 | | public IReadOnlyList<ParentalRating> GetParentalRatings() |
| | 193 | | { |
| | 194 | | // Use server default language for ratings |
| | 195 | | // Fall back to empty list if there are no parental ratings for that language |
| 2 | 196 | | var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? [] |
| | 197 | |
|
| | 198 | | // Add common ratings to ensure them being available for selection |
| | 199 | | // Based on the US rating system due to it being the main source of rating in the metadata providers |
| | 200 | | // Unrated |
| 2 | 201 | | if (!ratings.Any(x => x is null)) |
| | 202 | | { |
| 2 | 203 | | ratings.Add(new("Unrated", null)); |
| | 204 | | } |
| | 205 | |
|
| | 206 | | // Minimum rating possible |
| 2 | 207 | | if (ratings.All(x => x.RatingScore?.Score != 0)) |
| | 208 | | { |
| 0 | 209 | | ratings.Add(new("Approved", new(0, null))); |
| | 210 | | } |
| | 211 | |
|
| | 212 | | // Matches PG (this has different age restrictions depending on country) |
| 2 | 213 | | if (ratings.All(x => x.RatingScore?.Score != 10)) |
| | 214 | | { |
| 1 | 215 | | ratings.Add(new("10", new(10, null))); |
| | 216 | | } |
| | 217 | |
|
| | 218 | | // Matches PG-13 |
| 2 | 219 | | if (ratings.All(x => x.RatingScore?.Score != 13)) |
| | 220 | | { |
| 1 | 221 | | ratings.Add(new("13", new(13, null))); |
| | 222 | | } |
| | 223 | |
|
| | 224 | | // Matches TV-14 |
| 2 | 225 | | if (ratings.All(x => x.RatingScore?.Score != 14)) |
| | 226 | | { |
| 1 | 227 | | ratings.Add(new("14", new(14, null))); |
| | 228 | | } |
| | 229 | |
|
| | 230 | | // Catchall if max rating of country is less than 21 |
| | 231 | | // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned |
| 2 | 232 | | if (!ratings.Any(x => x.RatingScore?.Score >= 21)) |
| | 233 | | { |
| 2 | 234 | | ratings.Add(new ParentalRating("21", new(21, null))); |
| | 235 | | } |
| | 236 | |
|
| | 237 | | // A lot of countries don't explicitly have a separate rating for adult content |
| 2 | 238 | | if (ratings.All(x => x.RatingScore?.Score != 1000)) |
| | 239 | | { |
| 2 | 240 | | ratings.Add(new ParentalRating("XXX", new(1000, null))); |
| | 241 | | } |
| | 242 | |
|
| | 243 | | // A lot of countries don't explicitly have a separate rating for banned content |
| 2 | 244 | | if (ratings.All(x => x.RatingScore?.Score != 1001)) |
| | 245 | | { |
| 2 | 246 | | ratings.Add(new ParentalRating("Banned", new(1001, null))); |
| | 247 | | } |
| | 248 | |
|
| 2 | 249 | | return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; |
| | 250 | | } |
| | 251 | |
|
| | 252 | | /// <summary> |
| | 253 | | /// Gets the parental ratings dictionary. |
| | 254 | | /// </summary> |
| | 255 | | /// <param name="countryCode">The optional two letter ISO language string.</param> |
| | 256 | | /// <returns><see cref="Dictionary{String, ParentalRatingScore}" />.</returns> |
| | 257 | | private Dictionary<string, ParentalRatingScore?>? GetParentalRatingsDictionary(string? countryCode = null) |
| | 258 | | { |
| | 259 | | // Fallback to server default if no country code is specified. |
| 18 | 260 | | if (string.IsNullOrEmpty(countryCode)) |
| | 261 | | { |
| 16 | 262 | | countryCode = _configurationManager.Configuration.MetadataCountryCode; |
| | 263 | | } |
| | 264 | |
|
| 18 | 265 | | if (_allParentalRatings.TryGetValue(countryCode, out var countryValue)) |
| | 266 | | { |
| 17 | 267 | | return countryValue; |
| | 268 | | } |
| | 269 | |
|
| 1 | 270 | | return null; |
| | 271 | | } |
| | 272 | |
|
| | 273 | | /// <inheritdoc /> |
| | 274 | | public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null) |
| | 275 | | { |
| 25 | 276 | | ArgumentException.ThrowIfNullOrEmpty(rating); |
| | 277 | |
|
| | 278 | | // Handle unrated content |
| 25 | 279 | | if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase)) |
| | 280 | | { |
| 4 | 281 | | return null; |
| | 282 | | } |
| | 283 | |
|
| | 284 | | // Convert ints directly |
| | 285 | | // This may override some of the locale specific age ratings (but those always map to the same age) |
| 21 | 286 | | if (int.TryParse(rating, out var ratingAge)) |
| | 287 | | { |
| 6 | 288 | | return new(ratingAge, null); |
| | 289 | | } |
| | 290 | |
|
| | 291 | | // Fairly common for some users to have "Rated R" in their rating field |
| 15 | 292 | | rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase) |
| 15 | 293 | | .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase) |
| 15 | 294 | | .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase) |
| 15 | 295 | | .Trim(); |
| | 296 | |
|
| | 297 | | // Use rating system matching the language |
| 15 | 298 | | if (!string.IsNullOrEmpty(countryCode)) |
| | 299 | | { |
| 1 | 300 | | var ratingsDictionary = GetParentalRatingsDictionary(countryCode); |
| 1 | 301 | | if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? valu |
| | 302 | | { |
| 1 | 303 | | return value; |
| | 304 | | } |
| | 305 | | } |
| | 306 | | else |
| | 307 | | { |
| | 308 | | // Fall back to server default language for ratings check |
| | 309 | | // If it has no ratings, use the US ratings |
| 14 | 310 | | var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); |
| 14 | 311 | | if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? valu |
| | 312 | | { |
| 8 | 313 | | return value; |
| | 314 | | } |
| | 315 | | } |
| | 316 | |
|
| | 317 | | // If we don't find anything, check all ratings systems |
| 236 | 318 | | foreach (var dictionary in _allParentalRatings.Values) |
| | 319 | | { |
| 113 | 320 | | if (dictionary.TryGetValue(rating, out var value)) |
| | 321 | | { |
| 2 | 322 | | return value; |
| | 323 | | } |
| | 324 | | } |
| | 325 | |
|
| | 326 | | // Try splitting by : to handle "Germany: FSK-18" |
| 4 | 327 | | if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) |
| | 328 | | { |
| 2 | 329 | | var ratingLevelRightPart = rating.AsSpan().RightPart(':'); |
| 2 | 330 | | if (ratingLevelRightPart.Length != 0) |
| | 331 | | { |
| 1 | 332 | | return GetRatingScore(ratingLevelRightPart.ToString()); |
| | 333 | | } |
| | 334 | | } |
| | 335 | |
|
| | 336 | | // Handle prefix country code to handle "DE-18" |
| 3 | 337 | | if (rating.Contains('-', StringComparison.OrdinalIgnoreCase)) |
| | 338 | | { |
| 2 | 339 | | var ratingSpan = rating.AsSpan(); |
| | 340 | |
|
| | 341 | | // Extract culture from country prefix |
| 2 | 342 | | var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString()); |
| | 343 | |
|
| 2 | 344 | | var ratingLevelRightPart = ratingSpan.RightPart('-'); |
| 2 | 345 | | if (ratingLevelRightPart.Length != 0) |
| | 346 | | { |
| | 347 | | // Check rating system of culture |
| 1 | 348 | | return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); |
| | 349 | | } |
| | 350 | | } |
| | 351 | |
|
| 2 | 352 | | return null; |
| 2 | 353 | | } |
| | 354 | |
|
| | 355 | | /// <inheritdoc /> |
| | 356 | | public string GetLocalizedString(string phrase) |
| | 357 | | { |
| 537 | 358 | | return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture); |
| | 359 | | } |
| | 360 | |
|
| | 361 | | /// <inheritdoc /> |
| | 362 | | public string GetLocalizedString(string phrase, string culture) |
| | 363 | | { |
| 537 | 364 | | if (string.IsNullOrEmpty(culture)) |
| | 365 | | { |
| 0 | 366 | | culture = _configurationManager.Configuration.UICulture; |
| | 367 | | } |
| | 368 | |
|
| 537 | 369 | | if (string.IsNullOrEmpty(culture)) |
| | 370 | | { |
| 0 | 371 | | culture = DefaultCulture; |
| | 372 | | } |
| | 373 | |
|
| 537 | 374 | | var dictionary = GetLocalizationDictionary(culture); |
| | 375 | |
|
| 537 | 376 | | if (dictionary.TryGetValue(phrase, out var value)) |
| | 377 | | { |
| 515 | 378 | | return value; |
| | 379 | | } |
| | 380 | |
|
| 22 | 381 | | return phrase; |
| | 382 | | } |
| | 383 | |
|
| | 384 | | private Dictionary<string, string> GetLocalizationDictionary(string culture) |
| | 385 | | { |
| 537 | 386 | | ArgumentException.ThrowIfNullOrEmpty(culture); |
| | 387 | |
|
| | 388 | | const string Prefix = "Core"; |
| | 389 | |
|
| 537 | 390 | | return _dictionaries.GetOrAdd( |
| 537 | 391 | | culture, |
| 537 | 392 | | static (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".j |
| 537 | 393 | | this); |
| | 394 | | } |
| | 395 | |
|
| | 396 | | private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) |
| | 397 | | { |
| | 398 | | ArgumentException.ThrowIfNullOrEmpty(culture); |
| | 399 | |
|
| | 400 | | var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
| | 401 | |
|
| | 402 | | var namespaceName = GetType().Namespace + "." + prefix; |
| | 403 | |
|
| | 404 | | await CopyInto(dictionary, namespaceName + "." + baseFilename).ConfigureAwait(false); |
| | 405 | | await CopyInto(dictionary, namespaceName + "." + GetResourceFilename(culture)).ConfigureAwait(false); |
| | 406 | |
|
| | 407 | | return dictionary; |
| | 408 | | } |
| | 409 | |
|
| | 410 | | private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath) |
| | 411 | | { |
| | 412 | | using var stream = _assembly.GetManifestResourceStream(resourcePath); |
| | 413 | | // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the c |
| | 414 | | if (stream is null) |
| | 415 | | { |
| | 416 | | _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath); |
| | 417 | | return; |
| | 418 | | } |
| | 419 | |
|
| | 420 | | var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).Configure |
| | 421 | | foreach (var key in dict.Keys) |
| | 422 | | { |
| | 423 | | dictionary[key] = dict[key]; |
| | 424 | | } |
| | 425 | | } |
| | 426 | |
|
| | 427 | | private static string GetResourceFilename(string culture) |
| | 428 | | { |
| 25 | 429 | | var parts = culture.Split('-'); |
| | 430 | |
|
| 25 | 431 | | if (parts.Length == 2) |
| | 432 | | { |
| 24 | 433 | | culture = parts[0].ToLowerInvariant() + "-" + parts[1].ToUpperInvariant(); |
| | 434 | | } |
| | 435 | | else |
| | 436 | | { |
| 1 | 437 | | culture = culture.ToLowerInvariant(); |
| | 438 | | } |
| | 439 | |
|
| 25 | 440 | | return culture + ".json"; |
| | 441 | | } |
| | 442 | |
|
| | 443 | | /// <inheritdoc /> |
| | 444 | | public IEnumerable<LocalizationOption> GetLocalizationOptions() |
| | 445 | | { |
| | 446 | | yield return new LocalizationOption("Afrikaans", "af"); |
| | 447 | | yield return new LocalizationOption("العربية", "ar"); |
| | 448 | | yield return new LocalizationOption("Беларуская", "be"); |
| | 449 | | yield return new LocalizationOption("Български", "bg-BG"); |
| | 450 | | yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn"); |
| | 451 | | yield return new LocalizationOption("Català", "ca"); |
| | 452 | | yield return new LocalizationOption("Čeština", "cs"); |
| | 453 | | yield return new LocalizationOption("Cymraeg", "cy"); |
| | 454 | | yield return new LocalizationOption("Dansk", "da"); |
| | 455 | | yield return new LocalizationOption("Deutsch", "de"); |
| | 456 | | yield return new LocalizationOption("English (United Kingdom)", "en-GB"); |
| | 457 | | yield return new LocalizationOption("English", "en-US"); |
| | 458 | | yield return new LocalizationOption("Ελληνικά", "el"); |
| | 459 | | yield return new LocalizationOption("Esperanto", "eo"); |
| | 460 | | yield return new LocalizationOption("Español", "es"); |
| | 461 | | yield return new LocalizationOption("Español americano", "es_419"); |
| | 462 | | yield return new LocalizationOption("Español (Argentina)", "es-AR"); |
| | 463 | | yield return new LocalizationOption("Español (Dominicana)", "es_DO"); |
| | 464 | | yield return new LocalizationOption("Español (México)", "es-MX"); |
| | 465 | | yield return new LocalizationOption("Eesti", "et"); |
| | 466 | | yield return new LocalizationOption("Basque", "eu"); |
| | 467 | | yield return new LocalizationOption("فارسی", "fa"); |
| | 468 | | yield return new LocalizationOption("Suomi", "fi"); |
| | 469 | | yield return new LocalizationOption("Filipino", "fil"); |
| | 470 | | yield return new LocalizationOption("Français", "fr"); |
| | 471 | | yield return new LocalizationOption("Français (Canada)", "fr-CA"); |
| | 472 | | yield return new LocalizationOption("Galego", "gl"); |
| | 473 | | yield return new LocalizationOption("Schwiizerdütsch", "gsw"); |
| | 474 | | yield return new LocalizationOption("עִבְרִית", "he"); |
| | 475 | | yield return new LocalizationOption("हिन्दी", "hi"); |
| | 476 | | yield return new LocalizationOption("Hrvatski", "hr"); |
| | 477 | | yield return new LocalizationOption("Magyar", "hu"); |
| | 478 | | yield return new LocalizationOption("Bahasa Indonesia", "id"); |
| | 479 | | yield return new LocalizationOption("Íslenska", "is"); |
| | 480 | | yield return new LocalizationOption("Italiano", "it"); |
| | 481 | | yield return new LocalizationOption("日本語", "ja"); |
| | 482 | | yield return new LocalizationOption("Qazaqşa", "kk"); |
| | 483 | | yield return new LocalizationOption("한국어", "ko"); |
| | 484 | | yield return new LocalizationOption("Lietuvių", "lt"); |
| | 485 | | yield return new LocalizationOption("Latviešu", "lv"); |
| | 486 | | yield return new LocalizationOption("Македонски", "mk"); |
| | 487 | | yield return new LocalizationOption("മലയാളം", "ml"); |
| | 488 | | yield return new LocalizationOption("मराठी", "mr"); |
| | 489 | | yield return new LocalizationOption("Bahasa Melayu", "ms"); |
| | 490 | | yield return new LocalizationOption("Norsk bokmål", "nb"); |
| | 491 | | yield return new LocalizationOption("नेपाली", "ne"); |
| | 492 | | yield return new LocalizationOption("Nederlands", "nl"); |
| | 493 | | yield return new LocalizationOption("Norsk nynorsk", "nn"); |
| | 494 | | yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa"); |
| | 495 | | yield return new LocalizationOption("Polski", "pl"); |
| | 496 | | yield return new LocalizationOption("Pirate", "pr"); |
| | 497 | | yield return new LocalizationOption("Português", "pt"); |
| | 498 | | yield return new LocalizationOption("Português (Brasil)", "pt-BR"); |
| | 499 | | yield return new LocalizationOption("Português (Portugal)", "pt-PT"); |
| | 500 | | yield return new LocalizationOption("Românește", "ro"); |
| | 501 | | yield return new LocalizationOption("Русский", "ru"); |
| | 502 | | yield return new LocalizationOption("Slovenčina", "sk"); |
| | 503 | | yield return new LocalizationOption("Slovenščina", "sl-SI"); |
| | 504 | | yield return new LocalizationOption("Shqip", "sq"); |
| | 505 | | yield return new LocalizationOption("Српски", "sr"); |
| | 506 | | yield return new LocalizationOption("Svenska", "sv"); |
| | 507 | | yield return new LocalizationOption("தமிழ்", "ta"); |
| | 508 | | yield return new LocalizationOption("తెలుగు", "te"); |
| | 509 | | yield return new LocalizationOption("ภาษาไทย", "th"); |
| | 510 | | yield return new LocalizationOption("Türkçe", "tr"); |
| | 511 | | yield return new LocalizationOption("Українська", "uk"); |
| | 512 | | yield return new LocalizationOption("اُردُو", "ur_PK"); |
| | 513 | | yield return new LocalizationOption("Tiếng Việt", "vi"); |
| | 514 | | yield return new LocalizationOption("汉语 (简体字)", "zh-CN"); |
| | 515 | | yield return new LocalizationOption("漢語 (繁體字)", "zh-TW"); |
| | 516 | | yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); |
| | 517 | | } |
| | 518 | |
|
| | 519 | | /// <inheritdoc /> |
| | 520 | | public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT) |
| | 521 | | { |
| | 522 | | // Unlikely case the dictionary is not (yet) initialized properly |
| 3 | 523 | | if (_iso6392BtoT == null) |
| | 524 | | { |
| 0 | 525 | | isoT = null; |
| 0 | 526 | | return false; |
| | 527 | | } |
| | 528 | |
|
| 3 | 529 | | var result = _iso6392BtoT.TryGetValue(isoB, out isoT) && !string.IsNullOrEmpty(isoT); |
| | 530 | |
|
| | 531 | | // Ensure the ISO code being null if the result is false |
| 3 | 532 | | if (!result) |
| | 533 | | { |
| 1 | 534 | | isoT = null; |
| | 535 | | } |
| | 536 | |
|
| 3 | 537 | | return result; |
| | 538 | | } |
| | 539 | | } |
| | 540 | | } |