diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js index 8cc2896ba..bda7193d0 100644 --- a/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js @@ -5,25 +5,14 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions'; +import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; import SelectLanguageModalContent from './SelectLanguageModalContent'; function createMapStateToProps() { return createSelector( - (state) => state.settings.languageProfiles, - (languageProfiles) => { - const { - isSchemaFetching: isFetching, - isSchemaPopulated: isPopulated, - schemaError: error, - schema - } = languageProfiles; - - return { - isFetching, - isPopulated, - error, - items: schema.languages ? [...schema.languages].reverse() : [] - }; + createLanguagesSelector(), + (languages) => { + return languages; } ); } diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index c09227ab9..67d2d9ece 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -12,6 +12,7 @@ import { inputTypes } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import themes from 'Styles/Themes'; import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; export const firstDayOfWeekOptions = [ { key: 0, value: 'Sunday' }, @@ -57,6 +58,7 @@ class UISettings extends Component { hasSettings, onInputChange, onSavePress, + languages, ...otherProps } = this.props; @@ -72,17 +74,19 @@ class UISettings extends Component { { - isFetching && - + isFetching ? + : + null } { - !isFetching && error && -
Unable to load UI settings
+ !isFetching && error ? +
Unable to load UI settings
: + null } { - hasSettings && !isFetching && !error && + hasSettings && !isFetching && !error ?
-
+ +
+ + {translate('UI Language')} + + +
+ : + null }
@@ -205,6 +225,7 @@ UISettings.propTypes = { error: PropTypes.object, settings: PropTypes.object.isRequired, hasSettings: PropTypes.bool.isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, onSavePress: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/UI/UISettingsConnector.js b/frontend/src/Settings/UI/UISettingsConnector.js index b0fbb3339..7b7846415 100644 --- a/frontend/src/Settings/UI/UISettingsConnector.js +++ b/frontend/src/Settings/UI/UISettingsConnector.js @@ -3,30 +3,63 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions'; +import { fetchLanguageProfileSchema, fetchUISettings, saveUISettings, setUISettingsValue } from 'Store/Actions/settingsActions'; +import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import UISettings from './UISettings'; const SECTION = 'ui'; +const FILTER_LANGUAGES = ['Any', 'Unknown']; + +function createFilteredLanguagesSelector() { + return createSelector( + createLanguagesSelector(), + (languages) => { + if (!languages || !languages.items) { + return []; + } + + const newItems = languages.items + .filter((lang) => !FILTER_LANGUAGES.includes(lang.language.name)) + .map((item) => { + return { + key: item.language.id, + value: item.language.name + }; + }); + + return { + ...languages, + items: newItems + }; + } + ); +} function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, createSettingsSectionSelector(SECTION), - (advancedSettings, sectionSettings) => { + createFilteredLanguagesSelector(), + (advancedSettings, sectionSettings, languages) => { return { advancedSettings, - ...sectionSettings + languages: languages.items, + isLanguagesPopulated: languages.isPopulated, + ...sectionSettings, + isFetching: sectionSettings.isFetching || languages.isFetching, + error: sectionSettings.error || languages.error }; } ); } const mapDispatchToProps = { - setUISettingsValue, - saveUISettings, - fetchUISettings, - clearPendingChanges + dispatchSetUISettingsValue: setUISettingsValue, + dispatchSaveUISettings: saveUISettings, + dispatchFetchUISettings: fetchUISettings, + dispatchClearPendingChanges: clearPendingChanges, + dispatchFetchLanguageProfileSchema: fetchLanguageProfileSchema }; class UISettingsConnector extends Component { @@ -35,22 +68,32 @@ class UISettingsConnector extends Component { // Lifecycle componentDidMount() { - this.props.fetchUISettings(); + const { + isLanguagesPopulated, + dispatchFetchUISettings, + dispatchFetchLanguageProfileSchema + } = this.props; + + dispatchFetchUISettings(); + + if (!isLanguagesPopulated) { + dispatchFetchLanguageProfileSchema(); + } } componentWillUnmount() { - this.props.clearPendingChanges({ section: `settings.${SECTION}` }); + this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); } // // Listeners onInputChange = ({ name, value }) => { - this.props.setUISettingsValue({ name, value }); + this.props.dispatchSetUISettingsValue({ name, value }); }; onSavePress = () => { - this.props.saveUISettings(); + this.props.dispatchSaveUISettings(); }; // @@ -68,10 +111,12 @@ class UISettingsConnector extends Component { } UISettingsConnector.propTypes = { - setUISettingsValue: PropTypes.func.isRequired, - saveUISettings: PropTypes.func.isRequired, - fetchUISettings: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired + isLanguagesPopulated: PropTypes.bool.isRequired, + dispatchSetUISettingsValue: PropTypes.func.isRequired, + dispatchSaveUISettings: PropTypes.func.isRequired, + dispatchFetchUISettings: PropTypes.func.isRequired, + dispatchClearPendingChanges: PropTypes.func.isRequired, + dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(UISettingsConnector); diff --git a/frontend/src/Store/Selectors/createLanguagesSelector.js b/frontend/src/Store/Selectors/createLanguagesSelector.js new file mode 100644 index 000000000..53de7d696 --- /dev/null +++ b/frontend/src/Store/Selectors/createLanguagesSelector.js @@ -0,0 +1,24 @@ +import { createSelector } from 'reselect'; + +function createLanguagesSelector() { + return createSelector( + (state) => state.settings.languageProfiles, + (languageProfiles) => { + const { + isSchemaFetching: isFetching, + isSchemaPopulated: isPopulated, + schemaError: error, + schema + } = languageProfiles; + + return { + isFetching, + isPopulated, + error, + items: schema.languages ? [...schema.languages].reverse() : [] + }; + } + ); +} + +export default createLanguagesSelector; diff --git a/frontend/src/Utilities/String/translate.js b/frontend/src/Utilities/String/translate.js new file mode 100644 index 000000000..cef8ce047 --- /dev/null +++ b/frontend/src/Utilities/String/translate.js @@ -0,0 +1,28 @@ +import createAjaxRequest from 'Utilities/createAjaxRequest'; + +function getTranslations() { + return createAjaxRequest({ + global: false, + dataType: 'json', + url: '/localization' + }).request; +} + +let translations = {}; + +getTranslations().then((data) => { + translations = data.strings; +}); + +export default function translate(key, tokens) { + const translation = translations[key] || key; + + if (tokens) { + return translation.replace( + /\{([a-z0-9]+?)\}/gi, + (match, tokenMatch) => String(tokens[tokenMatch]) ?? match + ); + } + + return translation; +} diff --git a/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs new file mode 100644 index 000000000..79056ca81 --- /dev/null +++ b/src/NzbDrone.Core.Test/Localization/LocalizationServiceFixture.cs @@ -0,0 +1,88 @@ +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.UILanguage).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("UI Language"); + + localizedString.Should().Be("UI Language"); + } + + [Test] + public void should_get_string_in_default_language_dictionary_if_no_lang_country_code_exists_and_string_exists() + { + var localizedString = Subject.GetLocalizedString("UI Language", "fr_fr"); + + localizedString.Should().Be("UI Langue"); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists() + { + var localizedString = Subject.GetLocalizedString("UI Language", "an"); + + localizedString.Should().Be("UI Language"); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists() + { + var localizedString = Subject.GetLocalizedString("UI Language", ""); + + localizedString.Should().Be("UI Language"); + } + + [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/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index ae8797668..02d7d3f32 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Http.Proxy; using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Events; @@ -304,6 +305,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("EnableColorImpairedMode", value); } } + public int UILanguage + { + get { return GetValueInt("UILanguage", (int)Language.English); } + + set { SetValue("UILanguage", value); } + } + public bool CleanupMetadataImages { get { return GetValueBoolean("CleanupMetadataImages", true); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index e38402659..9190d8d7d 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -58,6 +58,7 @@ namespace NzbDrone.Core.Configuration string TimeFormat { get; set; } bool ShowRelativeDates { get; set; } bool EnableColorImpairedMode { get; set; } + int UILanguage { get; set; } //Internal bool CleanupMetadataImages { get; set; } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json new file mode 100644 index 000000000..7497a6dc6 --- /dev/null +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -0,0 +1,6 @@ +{ + "Language": "Language", + "UI Language": "UI Language", + "Language that Sonarr will use for UI": "Language that Sonarr will use for UI", + "Browser Reload Required": "Browser Reload Required" +} diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json new file mode 100644 index 000000000..eae05c73a --- /dev/null +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -0,0 +1,6 @@ +{ + "Language": "Langue", + "UI Language": "UI Langue", + "Language that Sonarr will use for UI": "Langue que Sonarr utilisera pour l'interface utilisateur", + "Browser Reload Required": "Rechargement du navigateur requis" +} diff --git a/src/NzbDrone.Core/Localization/LocalizationService.cs b/src/NzbDrone.Core/Localization/LocalizationService.cs new file mode 100644 index 000000000..606639f38 --- /dev/null +++ b/src/NzbDrone.Core/Localization/LocalizationService.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +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 = GetSetLanguageFileName(); + + return GetLocalizationDictionary(language); + } + + public string GetLocalizedString(string phrase) + { + var language = GetSetLanguageFileName(); + + return GetLocalizedString(phrase, language); + } + + public string GetLocalizedString(string phrase, string language) + { + if (string.IsNullOrEmpty(phrase)) + { + throw new ArgumentNullException(nameof(phrase)); + } + + if (language.IsNullOrWhiteSpace()) + { + language = GetSetLanguageFileName(); + } + + if (language == null) + { + language = DefaultCulture; + } + + var dictionary = GetLocalizationDictionary(language); + + if (dictionary.TryGetValue(phrase, out var value)) + { + return value; + } + + return phrase; + } + + private string GetSetLanguageFileName() + { + var isoLanguage = IsoLanguages.Get((Language)_configService.UILanguage); + var language = isoLanguage.TwoLetterCode; + + if (isoLanguage.CountryCode.IsNotNullOrWhiteSpace()) + { + language = string.Format("{0}_{1}", language, isoLanguage.CountryCode); + } + + return language; + } + + 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); + + if (culture.Contains("_")) + { + var languageBaseFilenamePath = Path.Combine(prefix, GetResourceFilename(culture.Split('_')[0])); + await CopyInto(dictionary, languageBaseFilenamePath).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.OpenText(resourcePath); + var json = await fs.ReadToEndAsync(); + var dict = Json.Deserialize>(json); + + foreach (var key in dict.Keys) + { + dictionary[key] = dict[key]; + } + } + + 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/Parser/IsoLanguage.cs b/src/NzbDrone.Core/Parser/IsoLanguage.cs index 78101bcb8..6d8f9bca8 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguage.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguage.cs @@ -6,12 +6,14 @@ namespace NzbDrone.Core.Parser { public string TwoLetterCode { get; set; } public string ThreeLetterCode { get; set; } + public string CountryCode { get; set; } public Language Language { get; set; } - public IsoLanguage(string twoLetterCode, string threeLetterCode, Language language) + public IsoLanguage(string twoLetterCode, string countryCode, string threeLetterCode, Language language) { TwoLetterCode = twoLetterCode; ThreeLetterCode = threeLetterCode; + CountryCode = countryCode; Language = language; } } diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index 3da24f3fc..060f29fda 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -10,38 +10,37 @@ namespace NzbDrone.Core.Parser { private static readonly HashSet All = new HashSet { - new IsoLanguage("en", "eng", Language.English), - new IsoLanguage("fr", "fra", Language.French), - new IsoLanguage("es", "spa", Language.Spanish), - new IsoLanguage("de", "deu", Language.German), - new IsoLanguage("it", "ita", Language.Italian), - new IsoLanguage("da", "dan", Language.Danish), - new IsoLanguage("nl", "nld", Language.Dutch), - new IsoLanguage("ja", "jpn", Language.Japanese), - new IsoLanguage("is", "isl", Language.Icelandic), - new IsoLanguage("zh", "zho", Language.Chinese), - new IsoLanguage("ru", "rus", Language.Russian), - new IsoLanguage("pl", "pol", Language.Polish), - new IsoLanguage("vi", "vie", Language.Vietnamese), - new IsoLanguage("sv", "swe", Language.Swedish), - new IsoLanguage("no", "nor", Language.Norwegian), - new IsoLanguage("nb", "nob", Language.Norwegian), // Norwegian Bokmål - new IsoLanguage("fi", "fin", Language.Finnish), - new IsoLanguage("tr", "tur", Language.Turkish), - new IsoLanguage("pt", "por", Language.Portuguese), - -// new IsoLanguage("nl", "nld", Language.Flemish), - new IsoLanguage("el", "ell", Language.Greek), - new IsoLanguage("ko", "kor", Language.Korean), - new IsoLanguage("hu", "hun", Language.Hungarian), - new IsoLanguage("he", "heb", Language.Hebrew), - new IsoLanguage("lt", "lit", Language.Lithuanian), - new IsoLanguage("cs", "ces", Language.Czech), - new IsoLanguage("ar", "ara", Language.Arabic), - new IsoLanguage("hi", "hin", Language.Hindi), - new IsoLanguage("bg", "bul", Language.Bulgarian), - new IsoLanguage("ml", "mal", Language.Malayalam), - new IsoLanguage("uk", "ukr", Language.Ukrainian), + new IsoLanguage("en", "", "eng", Language.English), + new IsoLanguage("fr", "fr", "fra", Language.French), + new IsoLanguage("es", "", "spa", Language.Spanish), + new IsoLanguage("de", "de", "deu", Language.German), + new IsoLanguage("it", "", "ita", Language.Italian), + new IsoLanguage("da", "", "dan", Language.Danish), + new IsoLanguage("nl", "", "nld", Language.Dutch), + new IsoLanguage("ja", "", "jpn", Language.Japanese), + new IsoLanguage("is", "", "isl", Language.Icelandic), + new IsoLanguage("zh", "cn", "zho", Language.Chinese), + new IsoLanguage("ru", "", "rus", Language.Russian), + new IsoLanguage("pl", "", "pol", Language.Polish), + new IsoLanguage("vi", "", "vie", Language.Vietnamese), + new IsoLanguage("sv", "", "swe", Language.Swedish), + new IsoLanguage("no", "", "nor", Language.Norwegian), + new IsoLanguage("nb", "", "nob", Language.Norwegian), // Norwegian Bokmål + new IsoLanguage("fi", "", "fin", Language.Finnish), + new IsoLanguage("tr", "", "tur", Language.Turkish), + new IsoLanguage("pt", "pt", "por", Language.Portuguese), + new IsoLanguage("nl", "", "nld", Language.Flemish), + new IsoLanguage("el", "", "ell", Language.Greek), + new IsoLanguage("ko", "", "kor", Language.Korean), + new IsoLanguage("hu", "", "hun", Language.Hungarian), + new IsoLanguage("he", "", "heb", Language.Hebrew), + new IsoLanguage("lt", "", "lit", Language.Lithuanian), + new IsoLanguage("cs", "", "ces", Language.Czech), + new IsoLanguage("ar", "", "ara", Language.Arabic), + new IsoLanguage("hi", "", "hin", Language.Hindi), + new IsoLanguage("bg", "", "bul", Language.Bulgarian), + new IsoLanguage("ml", "", "mal", Language.Malayalam), + new IsoLanguage("uk", "", "ukr", Language.Ukrainian), }; public static IsoLanguage Find(string isoCode) diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index e02afe810..1eff2ac6d 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -30,4 +30,9 @@ Resources\Logo\64.png + + + PreserveNewest + + diff --git a/src/Sonarr.Api.V3/Config/UiConfigResource.cs b/src/Sonarr.Api.V3/Config/UiConfigResource.cs index cab394c03..7bddae1ad 100644 --- a/src/Sonarr.Api.V3/Config/UiConfigResource.cs +++ b/src/Sonarr.Api.V3/Config/UiConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Configuration; +using NzbDrone.Core.Configuration; using Sonarr.Http.REST; namespace Sonarr.Api.V3.Config @@ -17,6 +17,7 @@ namespace Sonarr.Api.V3.Config public bool EnableColorImpairedMode { get; set; } public string Theme { get; set; } + public int UILanguage { get; set; } } public static class UiConfigResourceMapper @@ -34,7 +35,8 @@ namespace Sonarr.Api.V3.Config ShowRelativeDates = model.ShowRelativeDates, EnableColorImpairedMode = model.EnableColorImpairedMode, - Theme = config.Theme + Theme = config.Theme, + UILanguage = model.UILanguage }; } } diff --git a/src/Sonarr.Api.V3/Localization/LocalizationController.cs b/src/Sonarr.Api.V3/Localization/LocalizationController.cs new file mode 100644 index 000000000..e350b8497 --- /dev/null +++ b/src/Sonarr.Api.V3/Localization/LocalizationController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Localization; +using Sonarr.Http; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Localization +{ + [V3ApiController] + public class LocalizationController : RestController + { + private readonly ILocalizationService _localizationService; + + public LocalizationController(ILocalizationService localizationService) + { + _localizationService = localizationService; + } + + protected override LocalizationResource GetResourceById(int id) + { + return GetLocalization(); + } + + [HttpGet] + [Produces("application/json")] + public LocalizationResource GetLocalization() + { + return _localizationService.GetLocalizationDictionary().ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Localization/LocalizationResource.cs b/src/Sonarr.Api.V3/Localization/LocalizationResource.cs new file mode 100644 index 000000000..c2058b381 --- /dev/null +++ b/src/Sonarr.Api.V3/Localization/LocalizationResource.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V3.Localization +{ + public class LocalizationResourceSerializer : JsonConverter> + { + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, Dictionary dictionary, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var (key, value) in dictionary) + { + var propertyName = key; + writer.WritePropertyName(propertyName); + writer.WriteStringValue(value); + } + + writer.WriteEndObject(); + } + } + + public class LocalizationResource : RestResource + { + [JsonConverter(typeof(LocalizationResourceSerializer))] + 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, + }; + } + } +}