diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 0621c83a72..99f72b8fb8 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Threading; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -23,13 +24,21 @@ namespace Umbraco.Core.Models internal static CultureInfo GetUserCulture(string userLanguage, ILocalizedTextService textService) { - return textService.GetSupportedCultures() - .FirstOrDefault(culture => - //match on full name first - culture.Name.InvariantEquals(userLanguage.Replace("_", "-")) || - //then match on the 2 letter name - culture.TwoLetterISOLanguageName.InvariantEquals(userLanguage)); - } + try + { + var culture = CultureInfo.GetCultureInfo(userLanguage); + //TODO: This is a hack because we store the user language as 2 chars instead of the full culture + // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt + // to convert to a supported full culture + var result = textService.ConvertToSupportedCultureWithRegionCode(culture); + return result; + } + catch (CultureNotFoundException) + { + //return the default one + return CultureInfo.GetCultureInfo("en"); + } + } /// /// Checks if the user has access to the content item based on their start noe diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs index de95f24efa..9875d63170 100644 --- a/src/Umbraco.Core/Services/ILocalizedTextService.cs +++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs @@ -33,5 +33,19 @@ namespace Umbraco.Core.Services /// /// IEnumerable GetSupportedCultures(); + + /// + /// Tries to resolve a full 4 letter culture from a 2 letter culture name + /// + /// + /// 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 + /// + /// + /// + /// 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. + /// + CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture); } } diff --git a/src/Umbraco.Core/Services/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs index c9d05f3a3b..68c1c8d5e1 100644 --- a/src/Umbraco.Core/Services/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/LocalizedTextService.cs @@ -13,6 +13,7 @@ namespace Umbraco.Core.Services public class LocalizedTextService : ILocalizedTextService { + private readonly LocalizedTextServiceFileSources _fileSources; private readonly IDictionary>> _dictionarySource; private readonly IDictionary> _xmlSource; @@ -22,7 +23,8 @@ namespace Umbraco.Core.Services /// public LocalizedTextService(LocalizedTextServiceFileSources fileSources) { - _xmlSource = fileSources.GetXmlSources(); + if (fileSources == null) throw new ArgumentNullException("fileSources"); + _fileSources = fileSources; } /// @@ -49,6 +51,9 @@ namespace Umbraco.Core.Services { Mandate.ParameterNotNull(culture, "culture"); + //TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + //This is what the legacy ui service did if (string.IsNullOrEmpty(key)) return string.Empty; @@ -57,10 +62,14 @@ namespace Umbraco.Core.Services var area = keyParts.Length > 1 ? keyParts[0] : null; var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; - if (_xmlSource != null) + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.GetXmlSources() + : null); + + if (xmlSource != null) { - return GetFromXmlSource(culture, area, alias, tokens); - } + return GetFromXmlSource(xmlSource, culture, area, alias, tokens); + } else { return GetFromDictionarySource(culture, area, alias, tokens); @@ -75,18 +84,25 @@ namespace Umbraco.Core.Services { if (culture == null) throw new ArgumentNullException("culture"); + //TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + var result = new Dictionary(); - if (_xmlSource != null) + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.GetXmlSources() + : null); + + if (xmlSource != null) { - if (_xmlSource.ContainsKey(culture) == false) + if (xmlSource.ContainsKey(culture) == false) { LogHelper.Warn("The culture specified {0} was not found in any configured sources for this service", () => culture); return result; } //convert all areas + keys to a single key with a '/' - var areas = _xmlSource[culture].Value.XPathSelectElements("//area"); + var areas = xmlSource[culture].Value.XPathSelectElements("//area"); foreach (var area in areas) { var keys = area.XPathSelectElements("./key"); @@ -133,7 +149,36 @@ namespace Umbraco.Core.Services /// public IEnumerable GetSupportedCultures() { - return _xmlSource != null ? _xmlSource.Keys : _dictionarySource.Keys; + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.GetXmlSources() + : null); + + return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; + } + + /// + /// Tries to resolve a full 4 letter culture from a 2 letter culture name + /// + /// + /// 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 + /// + /// + /// + /// 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. + /// + /// This only works when this service is constructed with the LocalizedTextServiceFileSources + /// + public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture) + { + if (currentCulture == null) throw new ArgumentNullException("currentCulture"); + + if (_fileSources == null) return currentCulture; + if (currentCulture.Name.Length > 2) return currentCulture; + + var attempt = _fileSources.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName); + return attempt ? attempt.Result : currentCulture; } private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary tokens) @@ -174,15 +219,15 @@ namespace Umbraco.Core.Services return "[" + key + "]"; } - private string GetFromXmlSource(CultureInfo culture, string area, string key, IDictionary tokens) + private static string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) { - if (_xmlSource.ContainsKey(culture) == false) + if (xmlSource.ContainsKey(culture) == false) { LogHelper.Warn("The culture specified {0} was not found in any configured sources for this service", () => culture); return "[" + key + "]"; } - var cultureSource = _xmlSource[culture].Value; + var cultureSource = xmlSource[culture].Value; var xpath = area.IsNullOrWhiteSpace() ? string.Format("//key [@alias = '{0}']", key) @@ -213,7 +258,7 @@ namespace Umbraco.Core.Services /// we support a dictionary which means in the future we can really have any sort of token system. /// 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. /// - internal string ParseTokens(string value, IDictionary tokens) + internal static string ParseTokens(string value, IDictionary tokens) { if (tokens == null || tokens.Any() == false) { diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index 30e43a694c..be53d8d16b 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Xml; using System.Xml.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -16,6 +17,9 @@ namespace Umbraco.Core.Services private readonly IRuntimeCacheProvider _cache; private readonly DirectoryInfo _fileSourceFolder; + //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( + private readonly Dictionary _twoLetterCultureConverter = new Dictionary(); + public LocalizedTextServiceFileSources(IRuntimeCacheProvider cache, DirectoryInfo fileSourceFolder) { if (cache == null) throw new ArgumentNullException("cache"); @@ -46,10 +50,53 @@ namespace Umbraco.Core.Services { var localCopy = fileInfo; var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); - var culture = CultureInfo.GetCultureInfo(filename); + + //TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct + // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we + // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that + // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) + // that any 4 letter file is named with the actual culture that it is! + CultureInfo culture = null; + if (filename.Length == 2) + { + //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't + //want to load in the entire doc into mem just to read a single value + using (var fs = fileInfo.OpenRead()) + using (var reader = XmlReader.Create(fs)) + { + if (reader.IsStartElement()) + { + if (reader.Name == "language") + { + if (reader.MoveToAttribute("culture")) + { + var cultureVal = reader.Value; + try + { + culture = CultureInfo.GetCultureInfo(cultureVal); + //add to the tracked dictionary + _twoLetterCultureConverter[filename] = culture; + } + catch (CultureNotFoundException) + { + LogHelper.Warn( + string.Format("The culture {0} found in the file {1} is not a valid culture", cultureVal, fileInfo.FullName)); + //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise + // an exception will be thrown. + } + } + } + } + } + } + if (culture == null) + { + culture = CultureInfo.GetCultureInfo(filename); + } + //get the lazy value from cache result.Add(culture, new Lazy(() => _cache.GetCacheItem( - string.Format("{0}-{1}", typeof (LocalizedTextServiceFileSources).Name, culture.TwoLetterISOLanguageName), () => + string.Format("{0}-{1}", typeof (LocalizedTextServiceFileSources).Name, culture.Name), () => { using (var fs = localCopy.OpenRead()) { @@ -59,5 +106,15 @@ namespace Umbraco.Core.Services } return result; } + + //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( + public Attempt TryConvert2LetterCultureTo4Letter(string twoLetterCulture) + { + if (twoLetterCulture.Length != 2) Attempt.Fail(); + + return _twoLetterCultureConverter.ContainsKey(twoLetterCulture) + ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture]) + : Attempt.Fail(); + } } } \ No newline at end of file