LocalizationManager.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Model.Entities;
  12. using MediaBrowser.Model.Extensions;
  13. using MediaBrowser.Model.Globalization;
  14. using MediaBrowser.Model.Serialization;
  15. using Microsoft.Extensions.Logging;
  16. namespace Emby.Server.Implementations.Localization
  17. {
  18. /// <summary>
  19. /// Class LocalizationManager
  20. /// </summary>
  21. public class LocalizationManager : ILocalizationManager
  22. {
  23. /// <summary>
  24. /// The _configuration manager
  25. /// </summary>
  26. private readonly IServerConfigurationManager _configurationManager;
  27. /// <summary>
  28. /// The us culture
  29. /// </summary>
  30. private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
  31. private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
  32. new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase);
  33. private readonly IJsonSerializer _jsonSerializer;
  34. private readonly ILogger _logger;
  35. private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
  36. /// <summary>
  37. /// Initializes a new instance of the <see cref="LocalizationManager" /> class.
  38. /// </summary>
  39. /// <param name="configurationManager">The configuration manager.</param>
  40. /// <param name="jsonSerializer">The json serializer.</param>
  41. /// <param name="loggerFactory">The logger factory</param>
  42. public LocalizationManager(
  43. IServerConfigurationManager configurationManager,
  44. IJsonSerializer jsonSerializer,
  45. ILoggerFactory loggerFactory)
  46. {
  47. _configurationManager = configurationManager;
  48. _jsonSerializer = jsonSerializer;
  49. _logger = loggerFactory.CreateLogger(nameof(LocalizationManager));
  50. }
  51. public async Task LoadAll()
  52. {
  53. const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings.";
  54. // Extract from the assembly
  55. foreach (var resource in _assembly.GetManifestResourceNames())
  56. {
  57. if (!resource.StartsWith(RatingsResource, StringComparison.Ordinal))
  58. {
  59. continue;
  60. }
  61. string countryCode = resource.Substring(RatingsResource.Length, 2);
  62. var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
  63. using (var str = _assembly.GetManifestResourceStream(resource))
  64. using (var reader = new StreamReader(str))
  65. {
  66. string line;
  67. while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
  68. {
  69. if (string.IsNullOrWhiteSpace(line))
  70. {
  71. continue;
  72. }
  73. string[] parts = line.Split(',');
  74. if (parts.Length == 2
  75. && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
  76. {
  77. dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
  78. }
  79. #if DEBUG
  80. else
  81. {
  82. _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
  83. }
  84. #endif
  85. }
  86. }
  87. _allParentalRatings[countryCode] = dict;
  88. }
  89. await LoadCultures().ConfigureAwait(false);
  90. }
  91. public string NormalizeFormKD(string text)
  92. => text.Normalize(NormalizationForm.FormKD);
  93. private CultureDto[] _cultures;
  94. /// <summary>
  95. /// Gets the cultures.
  96. /// </summary>
  97. /// <returns>IEnumerable{CultureDto}.</returns>
  98. public CultureDto[] GetCultures()
  99. => _cultures;
  100. private async Task LoadCultures()
  101. {
  102. List<CultureDto> list = new List<CultureDto>();
  103. const string ResourcePath = "Emby.Server.Implementations.Localization.iso6392.txt";
  104. using (var stream = _assembly.GetManifestResourceStream(ResourcePath))
  105. using (var reader = new StreamReader(stream))
  106. {
  107. while (!reader.EndOfStream)
  108. {
  109. var line = await reader.ReadLineAsync().ConfigureAwait(false);
  110. if (string.IsNullOrWhiteSpace(line))
  111. {
  112. continue;
  113. }
  114. var parts = line.Split('|');
  115. if (parts.Length == 5)
  116. {
  117. string name = parts[3];
  118. if (string.IsNullOrWhiteSpace(name))
  119. {
  120. continue;
  121. }
  122. string twoCharName = parts[2];
  123. if (string.IsNullOrWhiteSpace(twoCharName))
  124. {
  125. continue;
  126. }
  127. string[] threeletterNames;
  128. if (string.IsNullOrWhiteSpace(parts[1]))
  129. {
  130. threeletterNames = new[] { parts[0] };
  131. }
  132. else
  133. {
  134. threeletterNames = new[] { parts[0], parts[1] };
  135. }
  136. list.Add(new CultureDto
  137. {
  138. DisplayName = name,
  139. Name = name,
  140. ThreeLetterISOLanguageNames = threeletterNames,
  141. TwoLetterISOLanguageName = twoCharName
  142. });
  143. }
  144. }
  145. }
  146. _cultures = list.ToArray();
  147. }
  148. public CultureDto FindLanguageInfo(string language)
  149. => GetCultures()
  150. .FirstOrDefault(i =>
  151. string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase)
  152. || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase)
  153. || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase)
  154. || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
  155. /// <summary>
  156. /// Gets the countries.
  157. /// </summary>
  158. /// <returns>IEnumerable{CountryInfo}.</returns>
  159. public Task<CountryInfo[]> GetCountries()
  160. => _jsonSerializer.DeserializeFromStreamAsync<CountryInfo[]>(
  161. _assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
  162. /// <summary>
  163. /// Gets the parental ratings.
  164. /// </summary>
  165. /// <returns>IEnumerable{ParentalRating}.</returns>
  166. public IEnumerable<ParentalRating> GetParentalRatings()
  167. => GetParentalRatingsDictionary().Values;
  168. /// <summary>
  169. /// Gets the parental ratings dictionary.
  170. /// </summary>
  171. /// <returns>Dictionary{System.StringParentalRating}.</returns>
  172. private Dictionary<string, ParentalRating> GetParentalRatingsDictionary()
  173. {
  174. var countryCode = _configurationManager.Configuration.MetadataCountryCode;
  175. if (string.IsNullOrEmpty(countryCode))
  176. {
  177. countryCode = "us";
  178. }
  179. return GetRatings(countryCode) ?? GetRatings("us");
  180. }
  181. /// <summary>
  182. /// Gets the ratings.
  183. /// </summary>
  184. /// <param name="countryCode">The country code.</param>
  185. /// <returns>The ratings</returns>
  186. private Dictionary<string, ParentalRating> GetRatings(string countryCode)
  187. {
  188. _allParentalRatings.TryGetValue(countryCode, out var value);
  189. return value;
  190. }
  191. private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
  192. /// <inheritdoc />
  193. /// <summary>
  194. /// Gets the rating level.
  195. /// </summary>
  196. /// <param name="rating">Rating field</param>
  197. /// <returns>The rating level</returns>&gt;
  198. public int? GetRatingLevel(string rating)
  199. {
  200. if (string.IsNullOrEmpty(rating))
  201. {
  202. throw new ArgumentNullException(nameof(rating));
  203. }
  204. if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase))
  205. {
  206. return null;
  207. }
  208. // Fairly common for some users to have "Rated R" in their rating field
  209. rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
  210. var ratingsDictionary = GetParentalRatingsDictionary();
  211. if (ratingsDictionary.TryGetValue(rating, out ParentalRating value))
  212. {
  213. return value.Value;
  214. }
  215. // If we don't find anything check all ratings systems
  216. foreach (var dictionary in _allParentalRatings.Values)
  217. {
  218. if (dictionary.TryGetValue(rating, out value))
  219. {
  220. return value.Value;
  221. }
  222. }
  223. // Try splitting by : to handle "Germany: FSK 18"
  224. var index = rating.IndexOf(':');
  225. if (index != -1)
  226. {
  227. rating = rating.Substring(index).TrimStart(':').Trim();
  228. if (!string.IsNullOrWhiteSpace(rating))
  229. {
  230. return GetRatingLevel(rating);
  231. }
  232. }
  233. // TODO: Further improve by normalizing out all spaces and dashes
  234. return null;
  235. }
  236. public bool HasUnicodeCategory(string value, UnicodeCategory category)
  237. {
  238. foreach (var chr in value)
  239. {
  240. if (char.GetUnicodeCategory(chr) == category)
  241. {
  242. return true;
  243. }
  244. }
  245. return false;
  246. }
  247. public string GetLocalizedString(string phrase)
  248. {
  249. return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture);
  250. }
  251. public string GetLocalizedString(string phrase, string culture)
  252. {
  253. if (string.IsNullOrEmpty(culture))
  254. {
  255. culture = _configurationManager.Configuration.UICulture;
  256. }
  257. if (string.IsNullOrEmpty(culture))
  258. {
  259. culture = DefaultCulture;
  260. }
  261. var dictionary = GetLocalizationDictionary(culture);
  262. if (dictionary.TryGetValue(phrase, out var value))
  263. {
  264. return value;
  265. }
  266. return phrase;
  267. }
  268. private const string DefaultCulture = "en-US";
  269. private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
  270. new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
  271. public Dictionary<string, string> GetLocalizationDictionary(string culture)
  272. {
  273. if (string.IsNullOrEmpty(culture))
  274. {
  275. throw new ArgumentNullException(nameof(culture));
  276. }
  277. const string prefix = "Core";
  278. var key = prefix + culture;
  279. return _dictionaries.GetOrAdd(key,
  280. f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
  281. }
  282. private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
  283. {
  284. if (string.IsNullOrEmpty(culture))
  285. {
  286. throw new ArgumentNullException(nameof(culture));
  287. }
  288. var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  289. var namespaceName = GetType().Namespace + "." + prefix;
  290. await CopyInto(dictionary, namespaceName + "." + baseFilename).ConfigureAwait(false);
  291. await CopyInto(dictionary, namespaceName + "." + GetResourceFilename(culture)).ConfigureAwait(false);
  292. return dictionary;
  293. }
  294. private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
  295. {
  296. using (var stream = _assembly.GetManifestResourceStream(resourcePath))
  297. {
  298. // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
  299. if (stream != null)
  300. {
  301. var dict = await _jsonSerializer.DeserializeFromStreamAsync<Dictionary<string, string>>(stream).ConfigureAwait(false);
  302. foreach (var key in dict.Keys)
  303. {
  304. dictionary[key] = dict[key];
  305. }
  306. }
  307. else
  308. {
  309. _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
  310. }
  311. }
  312. }
  313. private static string GetResourceFilename(string culture)
  314. {
  315. var parts = culture.Split('-');
  316. if (parts.Length == 2)
  317. {
  318. culture = parts[0].ToLowerInvariant() + "-" + parts[1].ToUpperInvariant();
  319. }
  320. else
  321. {
  322. culture = culture.ToLowerInvariant();
  323. }
  324. return culture + ".json";
  325. }
  326. public LocalizationOption[] GetLocalizationOptions()
  327. => new LocalizationOption[]
  328. {
  329. new LocalizationOption("Arabic", "ar"),
  330. new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"),
  331. new LocalizationOption("Catalan", "ca"),
  332. new LocalizationOption("Chinese Simplified", "zh-CN"),
  333. new LocalizationOption("Chinese Traditional", "zh-TW"),
  334. new LocalizationOption("Croatian", "hr"),
  335. new LocalizationOption("Czech", "cs"),
  336. new LocalizationOption("Danish", "da"),
  337. new LocalizationOption("Dutch", "nl"),
  338. new LocalizationOption("English (United Kingdom)", "en-GB"),
  339. new LocalizationOption("English (United States)", "en-US"),
  340. new LocalizationOption("French", "fr"),
  341. new LocalizationOption("French (Canada)", "fr-CA"),
  342. new LocalizationOption("German", "de"),
  343. new LocalizationOption("Greek", "el"),
  344. new LocalizationOption("Hebrew", "he"),
  345. new LocalizationOption("Hungarian", "hu"),
  346. new LocalizationOption("Italian", "it"),
  347. new LocalizationOption("Kazakh", "kk"),
  348. new LocalizationOption("Korean", "ko"),
  349. new LocalizationOption("Lithuanian", "lt-LT"),
  350. new LocalizationOption("Malay", "ms"),
  351. new LocalizationOption("Norwegian Bokmål", "nb"),
  352. new LocalizationOption("Persian", "fa"),
  353. new LocalizationOption("Polish", "pl"),
  354. new LocalizationOption("Portuguese (Brazil)", "pt-BR"),
  355. new LocalizationOption("Portuguese (Portugal)", "pt-PT"),
  356. new LocalizationOption("Russian", "ru"),
  357. new LocalizationOption("Slovak", "sk"),
  358. new LocalizationOption("Slovenian (Slovenia)", "sl-SI"),
  359. new LocalizationOption("Spanish", "es"),
  360. new LocalizationOption("Spanish (Argentina)", "es-AR"),
  361. new LocalizationOption("Spanish (Mexico)", "es-MX"),
  362. new LocalizationOption("Swedish", "sv"),
  363. new LocalizationOption("Swiss German", "gsw"),
  364. new LocalizationOption("Turkish", "tr")
  365. };
  366. }
  367. }