2021-01-11 13:39:09 +11:00
using System ;
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 ;
2020-09-21 08:19:26 +02:00
using Microsoft.Extensions.Logging ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2014-12-17 15:19:03 +11:00
2021-02-23 12:24:51 +01:00
namespace Umbraco.Cms.Core.Services.Implement
2014-12-17 15:19:03 +11:00
{
2019-01-27 01:17:32 -05: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 : ILocalizedTextService
{
2020-09-21 08:19:26 +02:00
private readonly ILogger < LocalizedTextService > _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>
2020-09-21 08:19:26 +02:00
public LocalizedTextService ( Lazy < LocalizedTextServiceFileSources > fileSources , ILogger < LocalizedTextService > 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>
2020-09-21 08:19:26 +02:00
public LocalizedTextService ( IDictionary < CultureInfo , Lazy < XDocument > > source , ILogger < LocalizedTextService > 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>
2020-09-21 08:19:26 +02:00
public LocalizedTextService ( IDictionary < CultureInfo , IDictionary < string , IDictionary < string , string > > > source , ILogger < LocalizedTextService > 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
2019-01-27 01:17:32 -05:00
// TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
2015-03-06 16:01:49 +11:00
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>
public IDictionary < string , string > GetAllStoredValues ( CultureInfo culture )
{
if ( culture = = null ) throw new ArgumentNullException ( "culture" ) ;
2019-01-27 01:17:32 -05:00
// TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
2015-03-06 16:01:49 +11:00
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
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "The culture specified {Culture} 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
}
2021-01-11 13:39:09 +11:00
// convert all areas + keys to a single key with a '/'
2016-01-14 15:06:16 +01:00
result = GetStoredTranslations ( xmlSource , culture ) ;
2021-01-11 13:39:09 +11:00
// merge with the English file in case there's keys in there that don't exist in the local file
var englishCulture = CultureInfo . GetCultureInfo ( "en-US" ) ;
2016-01-14 15:06:16 +01:00
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 ) )
2021-01-11 13:39:09 +11:00
{
2016-01-14 15:06:16 +01:00
result . Add ( englishResult . Key , englishResult . Value ) ;
2021-01-11 13:39:09 +11:00
}
2014-12-17 16:19:42 +11:00
}
}
else
{
if ( _dictionarySource . ContainsKey ( culture ) = = false )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "The culture specified {Culture} 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
}
2021-01-11 13:39:09 +11:00
// convert all areas + keys to a single key with a '/'
2014-12-17 16:19:42 +11:00
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 ) ;
2021-01-11 13:39:09 +11:00
// 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.
2014-12-18 12:33:34 +11:00
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
}
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 )
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "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
}
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
{
2020-09-16 09:58:07 +02:00
_logger . LogWarning ( "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
}
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
}
}
}