LocalizationManager.cs 19 KB

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