2014-12-17 15:19:03 +11:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Xml;
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Core.Services
|
|
|
|
|
|
{
|
|
|
|
|
|
//TODO: Convert all of this over to Niels K's localization framework one day
|
|
|
|
|
|
|
|
|
|
|
|
public class LocalizedTextService : ILocalizedTextService
|
|
|
|
|
|
{
|
2015-01-07 17:23:24 +11:00
|
|
|
|
private readonly ILogger _logger;
|
2015-06-15 17:13:34 +02:00
|
|
|
|
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, Lazy<XDocument>> _xmlSource;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Initializes with a file sources instance
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="fileSources"></param>
|
2015-01-07 17:23:24 +11:00
|
|
|
|
/// <param name="logger"></param>
|
2015-06-15 17:13:34 +02:00
|
|
|
|
public LocalizedTextService(Lazy<LocalizedTextServiceFileSources> fileSources, ILogger logger)
|
2014-12-17 15:19:03 +11:00
|
|
|
|
{
|
2015-01-07 17:23:24 +11:00
|
|
|
|
if (logger == null) throw new ArgumentNullException("logger");
|
|
|
|
|
|
_logger = logger;
|
2015-03-06 16:01:49 +11:00
|
|
|
|
if (fileSources == null) throw new ArgumentNullException("fileSources");
|
|
|
|
|
|
_fileSources = fileSources;
|
2014-12-17 15:19:03 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Initializes with an XML source
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="source"></param>
|
2015-01-07 17:23:24 +11:00
|
|
|
|
/// <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");
|
2015-01-07 17:23:24 +11:00
|
|
|
|
if (logger == null) throw new ArgumentNullException("logger");
|
2014-12-17 15:19:03 +11:00
|
|
|
|
_xmlSource = source;
|
2015-01-07 17:23:24 +11:00
|
|
|
|
_logger = logger;
|
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>
|
2015-01-22 15:42:29 +11:00
|
|
|
|
/// <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));
|
2014-12-17 15:19:03 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
2014-12-18 12:45:04 +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));
|
2014-12-17 15:19:03 +11:00
|
|
|
|
|
2015-03-06 16:01:49 +11:00
|
|
|
|
//TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
|
|
|
|
|
|
culture = ConvertToSupportedCultureWithRegionCode(culture);
|
|
|
|
|
|
|
2014-12-17 15:19:03 +11:00
|
|
|
|
//This is what the legacy ui service did
|
|
|
|
|
|
if (string.IsNullOrEmpty(key))
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
var keyParts = key.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
var area = keyParts.Length > 1 ? keyParts[0] : null;
|
|
|
|
|
|
var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
|
|
|
|
|
|
|
2015-03-06 16:01:49 +11:00
|
|
|
|
var xmlSource = _xmlSource ?? (_fileSources != null
|
2015-06-15 17:13:34 +02:00
|
|
|
|
? _fileSources.Value.GetXmlSources()
|
2015-03-06 16:01:49 +11:00
|
|
|
|
: null);
|
|
|
|
|
|
|
|
|
|
|
|
if (xmlSource != null)
|
2014-12-17 15:19:03 +11:00
|
|
|
|
{
|
2015-03-06 16:01:49 +11:00
|
|
|
|
return GetFromXmlSource(xmlSource, culture, area, alias, tokens);
|
2017-07-20 11:21:28 +02:00
|
|
|
|
}
|
2014-12-17 15:19:03 +11:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2014-12-17 16:19:42 +11:00
|
|
|
|
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");
|
|
|
|
|
|
|
2015-03-06 16:01:49 +11:00
|
|
|
|
//TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
|
|
|
|
|
|
culture = ConvertToSupportedCultureWithRegionCode(culture);
|
|
|
|
|
|
|
2014-12-17 16:19:42 +11:00
|
|
|
|
var result = new Dictionary<string, string>();
|
|
|
|
|
|
|
2015-03-06 16:01:49 +11:00
|
|
|
|
var xmlSource = _xmlSource ?? (_fileSources != null
|
2015-06-15 17:13:34 +02:00
|
|
|
|
? _fileSources.Value.GetXmlSources()
|
2015-03-06 16:01:49 +11:00
|
|
|
|
: null);
|
|
|
|
|
|
|
|
|
|
|
|
if (xmlSource != null)
|
2014-12-17 16:19:42 +11:00
|
|
|
|
{
|
2015-03-06 16:01:49 +11:00
|
|
|
|
if (xmlSource.ContainsKey(culture) == false)
|
2014-12-17 16:19:42 +11:00
|
|
|
|
{
|
2015-01-07 17:23:24 +11:00
|
|
|
|
_logger.Warn<LocalizedTextService>("The culture specified {0} was not found in any configured sources for this service", () => culture);
|
2015-01-07 10:39:00 +11:00
|
|
|
|
return result;
|
2014-12-17 16:19:42 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//convert all areas + keys to a single key with a '/'
|
2016-01-14 15:06:16 +01:00
|
|
|
|
result = GetStoredTranslations(xmlSource, culture);
|
|
|
|
|
|
|
|
|
|
|
|
//merge with the english file in case there's keys in there that don't exist in the local file
|
|
|
|
|
|
var englishCulture = new CultureInfo("en-US");
|
|
|
|
|
|
if (culture.Equals(englishCulture) == false)
|
2014-12-17 16:19:42 +11:00
|
|
|
|
{
|
2016-01-14 15:06:16 +01:00
|
|
|
|
var englishResults = GetStoredTranslations(xmlSource, englishCulture);
|
|
|
|
|
|
foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false))
|
|
|
|
|
|
result.Add(englishResult.Key, englishResult.Value);
|
2014-12-17 16:19:42 +11:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_dictionarySource.ContainsKey(culture) == false)
|
|
|
|
|
|
{
|
2015-01-07 17:23:24 +11:00
|
|
|
|
_logger.Warn<LocalizedTextService>("The culture specified {0} was not found in any configured sources for this service", () => culture);
|
2015-01-07 10:39:00 +11:00
|
|
|
|
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-18 12:33:34 +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)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Add(dictionaryKey, key.Value);
|
|
|
|
|
|
}
|
2014-12-17 16:19:42 +11:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-01-14 15:06:16 +01:00
|
|
|
|
private Dictionary<string, string> GetStoredTranslations(IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo cult)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = new Dictionary<string, string>();
|
|
|
|
|
|
var areas = xmlSource[cult].Value.XPathSelectElements("//area");
|
|
|
|
|
|
foreach (var area in areas)
|
|
|
|
|
|
{
|
|
|
|
|
|
var keys = area.XPathSelectElements("./key");
|
|
|
|
|
|
foreach (var key in keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"),
|
|
|
|
|
|
(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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2014-12-17 17:08:12 +11:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns a list of all currently supported cultures
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public IEnumerable<CultureInfo> GetSupportedCultures()
|
|
|
|
|
|
{
|
2015-03-06 16:01:49 +11:00
|
|
|
|
var xmlSource = _xmlSource ?? (_fileSources != null
|
2015-06-15 17:13:34 +02:00
|
|
|
|
? _fileSources.Value.GetXmlSources()
|
2015-03-06 16:01:49 +11:00
|
|
|
|
: null);
|
|
|
|
|
|
|
|
|
|
|
|
return xmlSource != null ? xmlSource.Keys : _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
|
2015-03-06 16:01:49 +11:00
|
|
|
|
/// 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
|
|
|
|
///
|
2015-03-06 16:01:49 +11: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
|
|
|
|
|
2015-03-06 16:01:49 +11:00
|
|
|
|
if (_fileSources == null) return currentCulture;
|
|
|
|
|
|
if (currentCulture.Name.Length > 2) return currentCulture;
|
|
|
|
|
|
|
2015-06-15 17:13:34 +02:00
|
|
|
|
var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
|
2015-03-06 16:01:49 +11:00
|
|
|
|
return attempt ? attempt.Result : currentCulture;
|
2014-12-17 17:08:12 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
2015-08-20 16:07:46 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// HAAAAAAAAAAACK! Used for backwards compat to convert a user's real culture code to a region code - normally this would be two letters
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="currentCulture"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public string ConvertToRegionCodeFromSupportedCulture(CultureInfo currentCulture)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (currentCulture == null) throw new ArgumentNullException("currentCulture");
|
|
|
|
|
|
|
|
|
|
|
|
if (_fileSources == null) return currentCulture.Name;
|
2017-07-20 11:21:28 +02:00
|
|
|
|
|
2015-08-20 16:07:46 +02:00
|
|
|
|
var attempt = _fileSources.Value.TryConvert4LetterCultureTo2Letter(currentCulture);
|
2017-07-20 11:21:28 +02:00
|
|
|
|
return attempt
|
|
|
|
|
|
? attempt.Result
|
2015-08-20 16:07:46 +02:00
|
|
|
|
: currentCulture.Name;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2015-01-07 17:23:24 +11:00
|
|
|
|
_logger.Warn<LocalizedTextService>("The culture specified {0} 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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var cultureSource = _dictionarySource[culture];
|
2017-07-20 11:21:28 +02:00
|
|
|
|
|
2014-12-17 15:19:03 +11:00
|
|
|
|
string found;
|
|
|
|
|
|
if (area.IsNullOrWhiteSpace())
|
|
|
|
|
|
{
|
|
|
|
|
|
found = cultureSource
|
|
|
|
|
|
.SelectMany(x => x.Value)
|
|
|
|
|
|
.Where(keyvals => keyvals.Key.InvariantEquals(key))
|
|
|
|
|
|
.Select(x => x.Value)
|
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
found = cultureSource
|
|
|
|
|
|
.Where(areas => areas.Key.InvariantEquals(area))
|
|
|
|
|
|
.SelectMany(a => a.Value)
|
|
|
|
|
|
.Where(keyvals => keyvals.Key.InvariantEquals(key))
|
|
|
|
|
|
.Select(x => x.Value)
|
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2015-03-19 17:42:57 +11:00
|
|
|
|
private string GetFromXmlSource(IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo culture, string area, string key, IDictionary<string, string> tokens)
|
2014-12-17 15:19:03 +11:00
|
|
|
|
{
|
2015-03-06 16:01:49 +11:00
|
|
|
|
if (xmlSource.ContainsKey(culture) == false)
|
2014-12-17 15:19:03 +11:00
|
|
|
|
{
|
2015-01-07 17:23:24 +11:00
|
|
|
|
_logger.Warn<LocalizedTextService>("The culture specified {0} 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
|
|
|
|
}
|
|
|
|
|
|
|
2015-12-15 12:51:03 +01:00
|
|
|
|
var found = FindTranslation(xmlSource, culture, area, key);
|
2014-12-17 15:19:03 +11:00
|
|
|
|
|
2014-12-17 16:19:42 +11:00
|
|
|
|
if (found != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return ParseTokens(found.Value, tokens);
|
|
|
|
|
|
}
|
2017-07-20 11:21:28 +02:00
|
|
|
|
|
2015-12-15 12:51:03 +01:00
|
|
|
|
// Fall back to English by default if we can't find the key
|
2015-12-15 13:50:19 +01:00
|
|
|
|
found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key);
|
2015-12-15 12:51:03 +01:00
|
|
|
|
if (found != null)
|
|
|
|
|
|
return ParseTokens(found.Value, tokens);
|
2014-12-17 16:19:42 +11:00
|
|
|
|
|
2015-12-15 12:51:03 +01:00
|
|
|
|
// If it can't be found in either file, fall back to the default, showing just the key in square brackets
|
|
|
|
|
|
// 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 + "]";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2015-12-15 12:51:03 +01:00
|
|
|
|
private XElement FindTranslation(IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo culture, string area, string key)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cultureSource = xmlSource[culture].Value;
|
|
|
|
|
|
|
|
|
|
|
|
var xpath = area.IsNullOrWhiteSpace()
|
|
|
|
|
|
? string.Format("//key [@alias = '{0}']", key)
|
|
|
|
|
|
: string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key);
|
|
|
|
|
|
|
|
|
|
|
|
var found = cultureSource.XPathSelectElement(xpath);
|
|
|
|
|
|
return found;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
|
2015-03-06 16:01:49 +11:00
|
|
|
|
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.Format("{0}{1}{0}", "%", token.Key), token.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return value;
|
2014-12-17 15:19:03 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|