diff --git a/frontend/src/Utilities/String/translate.js b/frontend/src/Utilities/String/translate.js new file mode 100644 index 000000000..dafbb0959 --- /dev/null +++ b/frontend/src/Utilities/String/translate.js @@ -0,0 +1,28 @@ +import $ from 'jquery'; + +function getTranslations() { + let localization = null; + const ajaxOptions = { + async: false, + type: 'GET', + global: false, + dataType: 'json', + url: `${window.Radarr.apiRoot}/localization`, + success: function(data) { + localization = data.strings; + } + }; + + ajaxOptions.headers = ajaxOptions.headers || {}; + ajaxOptions.headers['X-Api-Key'] = window.Radarr.apiKey; + + $.ajax(ajaxOptions); + return localization; +} + +const translations = getTranslations(); + +export default function translate(key) { + const formatedKey = key.charAt(0).toLowerCase() + key.slice(1); + return translations[formatedKey] || key; +} diff --git a/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs new file mode 100644 index 000000000..6ed1f8909 --- /dev/null +++ b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs @@ -0,0 +1,78 @@ +using System; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Localization; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Localization +{ + [TestFixture] + public class LocalizationServiceFixture : CoreTest + { + [SetUp] + public void Setup() + { + Mocker.GetMock().Setup(m => m.MovieInfoLanguage).Returns((int)Language.English); + + Mocker.GetMock().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory); + } + + [Test] + public void should_get_string_in_dictionary_if_lang_exists_and_string_exists() + { + var localizedString = Subject.GetLocalizedString("BackupNow"); + + localizedString.Should().Be("Backup Now"); + } + + [Test] + public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists() + { + var localizedString = Subject.GetLocalizedString("BackupNow", "an"); + + localizedString.Should().Be("Backup Now"); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists() + { + var localizedString = Subject.GetLocalizedString("BackupNow", ""); + + localizedString.Should().Be("Backup Now"); + } + + [Test] + public void should_return_argument_if_string_doesnt_exists() + { + var localizedString = Subject.GetLocalizedString("BadString", "en"); + + localizedString.Should().Be("BadString"); + } + + [Test] + public void should_return_argument_if_string_doesnt_exists_default_lang() + { + var localizedString = Subject.GetLocalizedString("BadString"); + + localizedString.Should().Be("BadString"); + } + + [Test] + public void should_throw_if_empty_string_passed() + { + Assert.Throws(() => Subject.GetLocalizedString("")); + } + + [Test] + public void should_throw_if_null_string_passed() + { + Assert.Throws(() => Subject.GetLocalizedString(null)); + } + } +} diff --git a/src/NzbDrone.Core/Localization/LocalizationService.cs b/src/NzbDrone.Core/Localization/LocalizationService.cs new file mode 100644 index 000000000..ee6ea20f6 --- /dev/null +++ b/src/NzbDrone.Core/Localization/LocalizationService.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Localization +{ + public interface ILocalizationService + { + Dictionary GetLocalizationDictionary(); + string GetLocalizedString(string phrase); + string GetLocalizedString(string phrase, string language); + } + + public class LocalizationService : ILocalizationService, IHandleAsync + { + private const string DefaultCulture = "en"; + + private readonly ICached> _cache; + + private readonly IConfigService _configService; + private readonly IAppFolderInfo _appFolderInfo; + private readonly Logger _logger; + + public LocalizationService(IConfigService configService, + IAppFolderInfo appFolderInfo, + ICacheManager cacheManager, + Logger logger) + { + _configService = configService; + _appFolderInfo = appFolderInfo; + _cache = cacheManager.GetCache>(typeof(Dictionary), "localization"); + _logger = logger; + } + + public Dictionary GetLocalizationDictionary() + { + var language = IsoLanguages.Get((Language)_configService.MovieInfoLanguage).TwoLetterCode; + + return GetLocalizationDictionary(language); + } + + public string GetLocalizedString(string phrase) + { + var language = IsoLanguages.Get((Language)_configService.MovieInfoLanguage).TwoLetterCode; + + return GetLocalizedString(phrase, language); + } + + public string GetLocalizedString(string phrase, string language) + { + if (string.IsNullOrEmpty(phrase)) + { + throw new ArgumentNullException(nameof(phrase)); + } + + if (language.IsNullOrWhiteSpace()) + { + language = IsoLanguages.Get((Language)_configService.MovieInfoLanguage).TwoLetterCode; + } + + if (language == null) + { + language = DefaultCulture; + } + + var dictionary = GetLocalizationDictionary(language); + + if (dictionary.TryGetValue(phrase, out var value)) + { + return value; + } + + return phrase; + } + + private Dictionary GetLocalizationDictionary(string language) + { + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException(nameof(language)); + } + + var startupFolder = _appFolderInfo.StartUpFolder; + + var prefix = Path.Combine(startupFolder, "Localization", "Core"); + var key = prefix + language; + + return _cache.Get("localization", () => GetDictionary(prefix, language, DefaultCulture + ".json").GetAwaiter().GetResult()); + } + + private async Task> GetDictionary(string prefix, string culture, string baseFilename) + { + if (string.IsNullOrEmpty(culture)) + { + throw new ArgumentNullException(nameof(culture)); + } + + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var baseFilenamePath = Path.Combine(prefix, baseFilename); + var alternativeFilenamePath = Path.Combine(prefix, GetResourceFilename(culture)); + + await CopyInto(dictionary, baseFilenamePath).ConfigureAwait(false); + await CopyInto(dictionary, alternativeFilenamePath).ConfigureAwait(false); + + return dictionary; + } + + private async Task CopyInto(IDictionary dictionary, string resourcePath) + { + if (!File.Exists(resourcePath)) + { + _logger.Error("Missing translation/culture resource: {0}", resourcePath); + return; + } + + using (var fs = File.OpenRead(resourcePath)) + { + if (fs != null) + { + var dict = await JsonSerializer.DeserializeAsync>(fs); + + foreach (var key in dict.Keys) + { + dictionary[key] = dict[key]; + } + } + else + { + _logger.Error("Missing translation/culture resource: {0}", resourcePath); + } + } + } + + private static string GetResourceFilename(string culture) + { + var parts = culture.Split('-'); + + if (parts.Length == 2) + { + culture = parts[0].ToLowerInvariant() + "-" + parts[1].ToUpperInvariant(); + } + else + { + culture = culture.ToLowerInvariant(); + } + + return culture + ".json"; + } + + public void HandleAsync(ConfigSavedEvent message) + { + _cache.Clear(); + } + } +} diff --git a/src/NzbDrone.Core/Radarr.Core.csproj b/src/NzbDrone.Core/Radarr.Core.csproj index bb1968467..2c4f69ac7 100644 --- a/src/NzbDrone.Core/Radarr.Core.csproj +++ b/src/NzbDrone.Core/Radarr.Core.csproj @@ -33,6 +33,11 @@ Resources\Logo\64.png + + + PreserveNewest + + diff --git a/src/Radarr.Api.V3/Localization/LocalizationModule.cs b/src/Radarr.Api.V3/Localization/LocalizationModule.cs new file mode 100644 index 000000000..8bebc5de9 --- /dev/null +++ b/src/Radarr.Api.V3/Localization/LocalizationModule.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Localization; +using Radarr.Http; + +namespace Radarr.Api.V3.Localization +{ + public class LocalizationModule : RadarrRestModule + { + private readonly ILocalizationService _localizationService; + + public LocalizationModule(ILocalizationService localizationService) + { + _localizationService = localizationService; + + GetResourceSingle = GetLocalizationDictionary; + } + + private LocalizationResource GetLocalizationDictionary() + { + return _localizationService.GetLocalizationDictionary().ToResource(); + } + } +} diff --git a/src/Radarr.Api.V3/Localization/LocalizationResource.cs b/src/Radarr.Api.V3/Localization/LocalizationResource.cs new file mode 100644 index 000000000..f9f8ce0b6 --- /dev/null +++ b/src/Radarr.Api.V3/Localization/LocalizationResource.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Radarr.Http.REST; + +namespace Radarr.Api.V3.Localization +{ + public class LocalizationResource : RestResource + { + public Dictionary Strings { get; set; } + } + + public static class LocalizationResourceMapper + { + public static LocalizationResource ToResource(this Dictionary localization) + { + if (localization == null) + { + return null; + } + + return new LocalizationResource + { + Strings = localization, + }; + } + } +}