Files
Umbraco-CMS/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs

310 lines
14 KiB
C#
Raw Normal View History

2014-12-17 15:19:03 +11:00
using System;
using System.Collections;
2014-12-17 15:19:03 +11:00
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
2015-01-07 10:39:00 +11:00
using Umbraco.Core.Logging;
2014-12-17 15:19:03 +11:00
2017-12-28 09:18:09 +01:00
namespace Umbraco.Core.Services.Implement
2014-12-17 15:19:03 +11:00
{
// TODO: Convert all of this over to Niels K's localization framework one day
2014-12-17 15:19:03 +11:00
public class LocalizedTextService : ILocalizedTextService2
2014-12-17 15:19:03 +11:00
{
private readonly ILogger _logger;
private readonly Lazy<LocalizedTextServiceFileSources> _fileSources;
2014-12-17 15:19:03 +11:00
private readonly IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> _dictionarySource;
private readonly IDictionary<CultureInfo, IDictionary<string, string>> _noAreaDictionarySource;
private readonly char[] _splitter = new[] { '/' };
2014-12-17 15:19:03 +11:00
/// <summary>
/// Initializes with a file sources instance
/// </summary>
/// <param name="fileSources"></param>
/// <param name="logger"></param>
public LocalizedTextService(Lazy<LocalizedTextServiceFileSources> fileSources, ILogger logger)
2014-12-17 15:19:03 +11:00
{
if (logger == null) throw new ArgumentNullException("logger");
_logger = logger;
if (fileSources == null) throw new ArgumentNullException("fileSources");
var dictionaries = FileSourcesToDictionarySources(fileSources.Value);
_dictionarySource = dictionaries.WithArea;
_noAreaDictionarySource = dictionaries.WithoutArea;
_fileSources = fileSources;
2014-12-17 15:19:03 +11:00
}
private (IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> WithArea, IDictionary<CultureInfo, IDictionary<string, string>> WithoutArea) FileSourcesToDictionarySources(LocalizedTextServiceFileSources fileSources)
{
var xmlSources = fileSources.GetXmlSources();
return XmlSourcesToDictionarySources(xmlSources);
}
private (IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> WithArea, IDictionary<CultureInfo, IDictionary<string, string>> WithoutArea) XmlSourcesToDictionarySources(IDictionary<CultureInfo, Lazy<XDocument>> source)
{
var cultureDictionary = new Dictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>>();
var cultureNoAreaDictionary = new Dictionary<CultureInfo, IDictionary<string, string>>();
foreach (var xmlSource in source)
{
var areaAliaValue = GetAreaStoredTranslations(source, xmlSource.Key);
cultureDictionary.Add(xmlSource.Key, areaAliaValue);
var aliasValue = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
foreach (var area in areaAliaValue)
{
foreach (var alias in area.Value)
{
if (!aliasValue.ContainsKey(alias.Key))
{
aliasValue.Add(alias.Key, alias.Value);
}
}
}
cultureNoAreaDictionary.Add(xmlSource.Key, aliasValue);
}
return (cultureDictionary, cultureNoAreaDictionary);
}
2014-12-17 15:19:03 +11:00
/// <summary>
/// Initializes with an XML source
/// </summary>
/// <param name="source"></param>
/// <param name="logger"></param>
public LocalizedTextService(IDictionary<CultureInfo, Lazy<XDocument>> source, ILogger logger)
2014-12-17 15:19:03 +11:00
{
if (source == null) throw new ArgumentNullException("source");
if (logger == null) throw new ArgumentNullException("logger");
_logger = logger;
var dictionaries = XmlSourcesToDictionarySources(source);
_dictionarySource = dictionaries.WithArea;
_noAreaDictionarySource = dictionaries.WithoutArea;
2014-12-17 15:19:03 +11:00
}
2014-12-17 15:19:03 +11:00
/// <summary>
/// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
/// </summary>
/// <param name="source"></param>
/// <param name="logger"></param>
public LocalizedTextService(IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> source, ILogger logger)
2017-05-31 09:18:09 +02:00
{
_dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var cultureNoAreaDictionary = new Dictionary<CultureInfo, IDictionary<string, string>>();
foreach (var cultureDictionary in _dictionarySource)
{
var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key);
var aliasValue = new Dictionary<string, string>();
foreach (var area in areaAliaValue)
{
foreach (var alias in area.Value)
{
if (!aliasValue.ContainsKey(alias.Key))
{
aliasValue.Add(alias.Key, alias.Value);
}
}
}
cultureNoAreaDictionary.Add(cultureDictionary.Key, aliasValue);
}
_noAreaDictionarySource = cultureNoAreaDictionary;
2014-12-17 15:19:03 +11:00
}
public string Localize(string key, CultureInfo culture, IDictionary<string, string> tokens = null)
2014-12-17 15:19:03 +11:00
{
2017-05-31 09:18:09 +02:00
if (culture == null) throw new ArgumentNullException(nameof(culture));
2021-01-17 22:02:01 +13:00
//This is what the legacy ui service did
if (string.IsNullOrEmpty(key))
return string.Empty;
var keyParts = key.Split(_splitter, StringSplitOptions.RemoveEmptyEntries);
2014-12-17 15:19:03 +11:00
var area = keyParts.Length > 1 ? keyParts[0] : null;
var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
return Localize(area, alias, culture, tokens);
}
public string Localize(string area, string alias, CultureInfo culture, IDictionary<string, string> tokens = null)
{
if (culture == null) throw new ArgumentNullException(nameof(culture));
2021-01-17 22:02:01 +13:00
//This is what the legacy ui service did
if (string.IsNullOrEmpty(alias))
return string.Empty;
// TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
culture = ConvertToSupportedCultureWithRegionCode(culture);
return GetFromDictionarySource(culture, area, alias, tokens);
2014-12-17 15:19:03 +11:00
}
2014-12-17 16:19:42 +11:00
/// <summary>
/// Returns all key/values in storage for the given culture
/// </summary>
/// <returns></returns>
public IDictionary<string, string> GetAllStoredValues(CultureInfo culture)
{
if (culture == null) throw new ArgumentNullException("culture");
// TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
culture = ConvertToSupportedCultureWithRegionCode(culture);
2014-12-17 16:19:42 +11:00
var result = new Dictionary<string, string>();
if (_dictionarySource.ContainsKey(culture) == false)
2014-12-17 16:19:42 +11:00
{
_logger.Warn<LocalizedTextService>("The culture specified {Culture} was not found in any configured sources for this service", culture);
return result;
2014-12-17 16:19:42 +11:00
}
//convert all areas + keys to a single key with a '/'
foreach (var area in _dictionarySource[culture])
{
foreach (var key in area.Value)
2014-12-17 16:19:42 +11:00
{
var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
//i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
if (result.ContainsKey(dictionaryKey) == false)
2014-12-17 16:19:42 +11:00
{
result.Add(dictionaryKey, key.Value);
2014-12-17 16:19:42 +11:00
}
}
}
return result;
}
private Dictionary<string, IDictionary<string, string>> GetAreaStoredTranslations(IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo cult)
{
var overallResult = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
var areas = xmlSource[cult].Value.XPathSelectElements("//area");
foreach (var area in areas)
{
var result = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
var keys = area.XPathSelectElements("./key");
foreach (var key in keys)
{
var dictionaryKey =
(string)key.Attribute("alias");
//there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
if (result.ContainsKey(dictionaryKey) == false)
result.Add(dictionaryKey, key.Value);
}
overallResult.Add(area.Attribute("alias").Value, result);
}
return overallResult;
}
private Dictionary<string, IDictionary<string, string>> GetAreaStoredTranslations(IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> dictionarySource, CultureInfo cult)
{
var overallResult = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
var areaDict = dictionarySource[cult];
foreach (var area in areaDict)
{
var result = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
var keys = area.Value.Keys;
foreach (var key in keys)
{
//there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
if (result.ContainsKey(key) == false)
result.Add(key, area.Value[key]);
}
overallResult.Add(area.Key, result);
}
return overallResult;
}
/// <summary>
/// Returns a list of all currently supported cultures
/// </summary>
/// <returns></returns>
public IEnumerable<CultureInfo> GetSupportedCultures()
{
return _dictionarySource.Keys;
}
/// <summary>
/// Tries to resolve a full 4 letter culture from a 2 letter culture name
/// </summary>
/// <param name="currentCulture">
/// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned
/// </param>
/// <returns></returns>
/// <remarks>
2017-07-20 11:21:28 +02:00
/// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that
/// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts
/// to resolve the full culture if possible.
2017-07-20 11:21:28 +02:00
///
/// This only works when this service is constructed with the LocalizedTextServiceFileSources
/// </remarks>
public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture)
{
if (currentCulture == null) throw new ArgumentNullException("currentCulture");
2017-07-20 11:21:28 +02:00
if (_fileSources == null) return currentCulture;
if (currentCulture.Name.Length > 2) return currentCulture;
var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
return attempt ? attempt.Result : currentCulture;
}
2014-12-17 16:19:42 +11:00
private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary<string, string> tokens)
2014-12-17 15:19:03 +11:00
{
if (_dictionarySource.ContainsKey(culture) == false)
{
_logger.Warn<LocalizedTextService>("The culture specified {Culture} was not found in any configured sources for this service", culture);
2017-07-20 11:21:28 +02:00
return "[" + key + "]";
2014-12-17 15:19:03 +11:00
}
2017-07-20 11:21:28 +02:00
string found = null;
if (string.IsNullOrWhiteSpace(area))
2014-12-17 15:19:03 +11:00
{
_noAreaDictionarySource[culture].TryGetValue(key, out found);
2014-12-17 15:19:03 +11:00
}
else
{
if (_dictionarySource[culture].TryGetValue(area, out var areaDictionary))
{
areaDictionary.TryGetValue(key, out found);
}
2014-12-17 15:19:03 +11:00
}
2014-12-17 16:19:42 +11:00
if (found != null)
{
return ParseTokens(found, tokens);
}
2014-12-17 15:19:03 +11:00
//NOTE: Based on how legacy works, the default text does not contain the area, just the key
2014-12-17 16:19:42 +11:00
return "[" + key + "]";
2014-12-17 15:19:03 +11:00
}
2014-12-17 16:19:42 +11:00
/// <summary>
/// Parses the tokens in the value
/// </summary>
/// <param name="value"></param>
/// <param name="tokens"></param>
/// <returns></returns>
/// <remarks>
2017-07-20 11:21:28 +02:00
/// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol.
2014-12-17 16:19:42 +11:00
/// For example: hello %0%, you are %1% !
2017-07-20 11:21:28 +02:00
///
2014-12-17 16:19:42 +11:00
/// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service
2017-07-20 11:21:28 +02:00
/// we support a dictionary which means in the future we can really have any sort of token system.
2014-12-17 16:19:42 +11:00
/// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case.
/// </remarks>
internal static string ParseTokens(string value, IDictionary<string, string> tokens)
2014-12-17 16:19:42 +11:00
{
if (tokens == null || tokens.Any() == false)
{
return value;
}
foreach (var token in tokens)
{
value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
2014-12-17 16:19:42 +11:00
}
return value;
2014-12-17 15:19:03 +11:00
}
}
}