New: Add UI Localization Framework
This commit is contained in:
parent
1977f4aa3c
commit
5938a95abb
|
@ -5,25 +5,14 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||||
import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions';
|
import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions';
|
||||||
|
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||||
import SelectLanguageModalContent from './SelectLanguageModalContent';
|
import SelectLanguageModalContent from './SelectLanguageModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.languageProfiles,
|
createLanguagesSelector(),
|
||||||
(languageProfiles) => {
|
(languages) => {
|
||||||
const {
|
return languages;
|
||||||
isSchemaFetching: isFetching,
|
|
||||||
isSchemaPopulated: isPopulated,
|
|
||||||
schemaError: error,
|
|
||||||
schema
|
|
||||||
} = languageProfiles;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items: schema.languages ? [...schema.languages].reverse() : []
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { inputTypes } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import themes from 'Styles/Themes';
|
import themes from 'Styles/Themes';
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
export const firstDayOfWeekOptions = [
|
export const firstDayOfWeekOptions = [
|
||||||
{ key: 0, value: 'Sunday' },
|
{ key: 0, value: 'Sunday' },
|
||||||
|
@ -57,6 +58,7 @@ class UISettings extends Component {
|
||||||
hasSettings,
|
hasSettings,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
|
languages,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -72,17 +74,19 @@ class UISettings extends Component {
|
||||||
|
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching ?
|
||||||
<LoadingIndicator />
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error &&
|
!isFetching && error ?
|
||||||
<div>Unable to load UI settings</div>
|
<div>Unable to load UI settings</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
hasSettings && !isFetching && !error &&
|
hasSettings && !isFetching && !error ?
|
||||||
<Form
|
<Form
|
||||||
id="uiSettings"
|
id="uiSettings"
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
@ -191,7 +195,23 @@ class UISettings extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</Form>
|
|
||||||
|
<FieldSet legend={translate('Language')}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('UI Language')}</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="uiLanguage"
|
||||||
|
values={languages}
|
||||||
|
helpText={translate('Language that Sonarr will use for UI')}
|
||||||
|
helpTextWarning={translate('Browser Reload Required')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.uiLanguage}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</Form> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
@ -205,6 +225,7 @@ UISettings.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
settings: PropTypes.object.isRequired,
|
settings: PropTypes.object.isRequired,
|
||||||
hasSettings: PropTypes.bool.isRequired,
|
hasSettings: PropTypes.bool.isRequired,
|
||||||
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,30 +3,63 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
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 createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||||
import UISettings from './UISettings';
|
import UISettings from './UISettings';
|
||||||
|
|
||||||
const SECTION = 'ui';
|
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() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.advancedSettings,
|
(state) => state.settings.advancedSettings,
|
||||||
createSettingsSectionSelector(SECTION),
|
createSettingsSectionSelector(SECTION),
|
||||||
(advancedSettings, sectionSettings) => {
|
createFilteredLanguagesSelector(),
|
||||||
|
(advancedSettings, sectionSettings, languages) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
advancedSettings,
|
||||||
...sectionSettings
|
languages: languages.items,
|
||||||
|
isLanguagesPopulated: languages.isPopulated,
|
||||||
|
...sectionSettings,
|
||||||
|
isFetching: sectionSettings.isFetching || languages.isFetching,
|
||||||
|
error: sectionSettings.error || languages.error
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
setUISettingsValue,
|
dispatchSetUISettingsValue: setUISettingsValue,
|
||||||
saveUISettings,
|
dispatchSaveUISettings: saveUISettings,
|
||||||
fetchUISettings,
|
dispatchFetchUISettings: fetchUISettings,
|
||||||
clearPendingChanges
|
dispatchClearPendingChanges: clearPendingChanges,
|
||||||
|
dispatchFetchLanguageProfileSchema: fetchLanguageProfileSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
class UISettingsConnector extends Component {
|
class UISettingsConnector extends Component {
|
||||||
|
@ -35,22 +68,32 @@ class UISettingsConnector extends Component {
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUISettings();
|
const {
|
||||||
|
isLanguagesPopulated,
|
||||||
|
dispatchFetchUISettings,
|
||||||
|
dispatchFetchLanguageProfileSchema
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
dispatchFetchUISettings();
|
||||||
|
|
||||||
|
if (!isLanguagesPopulated) {
|
||||||
|
dispatchFetchLanguageProfileSchema();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
|
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
onInputChange = ({ name, value }) => {
|
||||||
this.props.setUISettingsValue({ name, value });
|
this.props.dispatchSetUISettingsValue({ name, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSavePress = () => {
|
onSavePress = () => {
|
||||||
this.props.saveUISettings();
|
this.props.dispatchSaveUISettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -68,10 +111,12 @@ class UISettingsConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
UISettingsConnector.propTypes = {
|
UISettingsConnector.propTypes = {
|
||||||
setUISettingsValue: PropTypes.func.isRequired,
|
isLanguagesPopulated: PropTypes.bool.isRequired,
|
||||||
saveUISettings: PropTypes.func.isRequired,
|
dispatchSetUISettingsValue: PropTypes.func.isRequired,
|
||||||
fetchUISettings: PropTypes.func.isRequired,
|
dispatchSaveUISettings: PropTypes.func.isRequired,
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(UISettingsConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(UISettingsConnector);
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<LocalizationService>
|
||||||
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English);
|
||||||
|
|
||||||
|
Mocker.GetMock<IAppFolderInfo>().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<ArgumentNullException>(() => Subject.GetLocalizedString(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_throw_if_null_string_passed()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ using NLog;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
using NzbDrone.Core.MediaFiles.EpisodeImport;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
@ -304,6 +305,13 @@ namespace NzbDrone.Core.Configuration
|
||||||
set { SetValue("EnableColorImpairedMode", value); }
|
set { SetValue("EnableColorImpairedMode", value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int UILanguage
|
||||||
|
{
|
||||||
|
get { return GetValueInt("UILanguage", (int)Language.English); }
|
||||||
|
|
||||||
|
set { SetValue("UILanguage", value); }
|
||||||
|
}
|
||||||
|
|
||||||
public bool CleanupMetadataImages
|
public bool CleanupMetadataImages
|
||||||
{
|
{
|
||||||
get { return GetValueBoolean("CleanupMetadataImages", true); }
|
get { return GetValueBoolean("CleanupMetadataImages", true); }
|
||||||
|
|
|
@ -58,6 +58,7 @@ namespace NzbDrone.Core.Configuration
|
||||||
string TimeFormat { get; set; }
|
string TimeFormat { get; set; }
|
||||||
bool ShowRelativeDates { get; set; }
|
bool ShowRelativeDates { get; set; }
|
||||||
bool EnableColorImpairedMode { get; set; }
|
bool EnableColorImpairedMode { get; set; }
|
||||||
|
int UILanguage { get; set; }
|
||||||
|
|
||||||
//Internal
|
//Internal
|
||||||
bool CleanupMetadataImages { get; set; }
|
bool CleanupMetadataImages { get; set; }
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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<string, string> GetLocalizationDictionary();
|
||||||
|
string GetLocalizedString(string phrase);
|
||||||
|
string GetLocalizedString(string phrase, string language);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LocalizationService : ILocalizationService, IHandleAsync<ConfigSavedEvent>
|
||||||
|
{
|
||||||
|
private const string DefaultCulture = "en";
|
||||||
|
|
||||||
|
private readonly ICached<Dictionary<string, string>> _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<Dictionary<string, string>>(typeof(Dictionary<string, string>), "localization");
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> 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<string, string> 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<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(culture))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(culture));
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<string, string>(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<string, string> 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<Dictionary<string, string>>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,12 +6,14 @@ namespace NzbDrone.Core.Parser
|
||||||
{
|
{
|
||||||
public string TwoLetterCode { get; set; }
|
public string TwoLetterCode { get; set; }
|
||||||
public string ThreeLetterCode { get; set; }
|
public string ThreeLetterCode { get; set; }
|
||||||
|
public string CountryCode { get; set; }
|
||||||
public Language Language { 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;
|
TwoLetterCode = twoLetterCode;
|
||||||
ThreeLetterCode = threeLetterCode;
|
ThreeLetterCode = threeLetterCode;
|
||||||
|
CountryCode = countryCode;
|
||||||
Language = language;
|
Language = language;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,38 +10,37 @@ namespace NzbDrone.Core.Parser
|
||||||
{
|
{
|
||||||
private static readonly HashSet<IsoLanguage> All = new HashSet<IsoLanguage>
|
private static readonly HashSet<IsoLanguage> All = new HashSet<IsoLanguage>
|
||||||
{
|
{
|
||||||
new IsoLanguage("en", "eng", Language.English),
|
new IsoLanguage("en", "", "eng", Language.English),
|
||||||
new IsoLanguage("fr", "fra", Language.French),
|
new IsoLanguage("fr", "fr", "fra", Language.French),
|
||||||
new IsoLanguage("es", "spa", Language.Spanish),
|
new IsoLanguage("es", "", "spa", Language.Spanish),
|
||||||
new IsoLanguage("de", "deu", Language.German),
|
new IsoLanguage("de", "de", "deu", Language.German),
|
||||||
new IsoLanguage("it", "ita", Language.Italian),
|
new IsoLanguage("it", "", "ita", Language.Italian),
|
||||||
new IsoLanguage("da", "dan", Language.Danish),
|
new IsoLanguage("da", "", "dan", Language.Danish),
|
||||||
new IsoLanguage("nl", "nld", Language.Dutch),
|
new IsoLanguage("nl", "", "nld", Language.Dutch),
|
||||||
new IsoLanguage("ja", "jpn", Language.Japanese),
|
new IsoLanguage("ja", "", "jpn", Language.Japanese),
|
||||||
new IsoLanguage("is", "isl", Language.Icelandic),
|
new IsoLanguage("is", "", "isl", Language.Icelandic),
|
||||||
new IsoLanguage("zh", "zho", Language.Chinese),
|
new IsoLanguage("zh", "cn", "zho", Language.Chinese),
|
||||||
new IsoLanguage("ru", "rus", Language.Russian),
|
new IsoLanguage("ru", "", "rus", Language.Russian),
|
||||||
new IsoLanguage("pl", "pol", Language.Polish),
|
new IsoLanguage("pl", "", "pol", Language.Polish),
|
||||||
new IsoLanguage("vi", "vie", Language.Vietnamese),
|
new IsoLanguage("vi", "", "vie", Language.Vietnamese),
|
||||||
new IsoLanguage("sv", "swe", Language.Swedish),
|
new IsoLanguage("sv", "", "swe", Language.Swedish),
|
||||||
new IsoLanguage("no", "nor", Language.Norwegian),
|
new IsoLanguage("no", "", "nor", Language.Norwegian),
|
||||||
new IsoLanguage("nb", "nob", Language.Norwegian), // Norwegian Bokmål
|
new IsoLanguage("nb", "", "nob", Language.Norwegian), // Norwegian Bokmål
|
||||||
new IsoLanguage("fi", "fin", Language.Finnish),
|
new IsoLanguage("fi", "", "fin", Language.Finnish),
|
||||||
new IsoLanguage("tr", "tur", Language.Turkish),
|
new IsoLanguage("tr", "", "tur", Language.Turkish),
|
||||||
new IsoLanguage("pt", "por", Language.Portuguese),
|
new IsoLanguage("pt", "pt", "por", Language.Portuguese),
|
||||||
|
new IsoLanguage("nl", "", "nld", Language.Flemish),
|
||||||
// new IsoLanguage("nl", "nld", Language.Flemish),
|
new IsoLanguage("el", "", "ell", Language.Greek),
|
||||||
new IsoLanguage("el", "ell", Language.Greek),
|
new IsoLanguage("ko", "", "kor", Language.Korean),
|
||||||
new IsoLanguage("ko", "kor", Language.Korean),
|
new IsoLanguage("hu", "", "hun", Language.Hungarian),
|
||||||
new IsoLanguage("hu", "hun", Language.Hungarian),
|
new IsoLanguage("he", "", "heb", Language.Hebrew),
|
||||||
new IsoLanguage("he", "heb", Language.Hebrew),
|
new IsoLanguage("lt", "", "lit", Language.Lithuanian),
|
||||||
new IsoLanguage("lt", "lit", Language.Lithuanian),
|
new IsoLanguage("cs", "", "ces", Language.Czech),
|
||||||
new IsoLanguage("cs", "ces", Language.Czech),
|
new IsoLanguage("ar", "", "ara", Language.Arabic),
|
||||||
new IsoLanguage("ar", "ara", Language.Arabic),
|
new IsoLanguage("hi", "", "hin", Language.Hindi),
|
||||||
new IsoLanguage("hi", "hin", Language.Hindi),
|
new IsoLanguage("bg", "", "bul", Language.Bulgarian),
|
||||||
new IsoLanguage("bg", "bul", Language.Bulgarian),
|
new IsoLanguage("ml", "", "mal", Language.Malayalam),
|
||||||
new IsoLanguage("ml", "mal", Language.Malayalam),
|
new IsoLanguage("uk", "", "ukr", Language.Ukrainian),
|
||||||
new IsoLanguage("uk", "ukr", Language.Ukrainian),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static IsoLanguage Find(string isoCode)
|
public static IsoLanguage Find(string isoCode)
|
||||||
|
|
|
@ -30,4 +30,9 @@
|
||||||
<Link>Resources\Logo\64.png</Link>
|
<Link>Resources\Logo\64.png</Link>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Localization\Core\**">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using Sonarr.Http.REST;
|
using Sonarr.Http.REST;
|
||||||
|
|
||||||
namespace Sonarr.Api.V3.Config
|
namespace Sonarr.Api.V3.Config
|
||||||
|
@ -17,6 +17,7 @@ namespace Sonarr.Api.V3.Config
|
||||||
|
|
||||||
public bool EnableColorImpairedMode { get; set; }
|
public bool EnableColorImpairedMode { get; set; }
|
||||||
public string Theme { get; set; }
|
public string Theme { get; set; }
|
||||||
|
public int UILanguage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UiConfigResourceMapper
|
public static class UiConfigResourceMapper
|
||||||
|
@ -34,7 +35,8 @@ namespace Sonarr.Api.V3.Config
|
||||||
ShowRelativeDates = model.ShowRelativeDates,
|
ShowRelativeDates = model.ShowRelativeDates,
|
||||||
|
|
||||||
EnableColorImpairedMode = model.EnableColorImpairedMode,
|
EnableColorImpairedMode = model.EnableColorImpairedMode,
|
||||||
Theme = config.Theme
|
Theme = config.Theme,
|
||||||
|
UILanguage = model.UILanguage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<LocalizationResource>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Dictionary<string, string>>
|
||||||
|
{
|
||||||
|
public override Dictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, Dictionary<string, string> 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<string, string> Strings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalizationResourceMapper
|
||||||
|
{
|
||||||
|
public static LocalizationResource ToResource(this Dictionary<string, string> localization)
|
||||||
|
{
|
||||||
|
if (localization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LocalizationResource
|
||||||
|
{
|
||||||
|
Strings = localization,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue