2022-02-28 15:01:18 +01:00
|
|
|
using System;
|
2014-12-18 12:45:04 +11:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.IO;
|
2015-06-15 17:13:34 +02:00
|
|
|
using System.Linq;
|
2015-03-06 16:01:49 +11:00
|
|
|
using System.Xml;
|
2014-12-18 12:45:04 +11:00
|
|
|
using System.Xml.Linq;
|
2022-05-02 19:38:33 +02:00
|
|
|
using Microsoft.Extensions.FileProviders;
|
|
|
|
|
using Microsoft.Extensions.FileProviders.Internal;
|
2020-09-16 13:08:27 +02:00
|
|
|
using Microsoft.Extensions.Logging;
|
2021-02-09 10:22:42 +01:00
|
|
|
using Umbraco.Cms.Core.Cache;
|
2021-02-09 11:26:22 +01:00
|
|
|
using Umbraco.Extensions;
|
2014-12-18 12:45:04 +11:00
|
|
|
|
2022-01-14 10:57:31 +00:00
|
|
|
namespace Umbraco.Cms.Core.Services
|
2014-12-18 12:45:04 +11:00
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Exposes the XDocument sources from files for the default localization text service and ensure caching is taken care of
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class LocalizedTextServiceFileSources
|
|
|
|
|
{
|
2020-09-21 08:19:26 +02:00
|
|
|
private readonly ILogger<LocalizedTextServiceFileSources> _logger;
|
2022-05-02 19:38:33 +02:00
|
|
|
private readonly IDirectoryContents _directoryContents;
|
2019-01-18 07:56:38 +01:00
|
|
|
private readonly IAppPolicyCache _cache;
|
2022-02-16 16:03:53 +01:00
|
|
|
private readonly IEnumerable<LocalizedTextServiceSupplementaryFileSource>? _supplementFileSources;
|
|
|
|
|
private readonly DirectoryInfo? _fileSourceFolder;
|
2014-12-18 12:45:04 +11:00
|
|
|
|
2019-01-27 01:17:32 -05:00
|
|
|
// 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 :(
|
2015-03-06 16:01:49 +11:00
|
|
|
private readonly Dictionary<string, CultureInfo> _twoLetterCultureConverter = new Dictionary<string, CultureInfo>();
|
|
|
|
|
|
2015-03-20 18:15:31 +11:00
|
|
|
private readonly Lazy<Dictionary<CultureInfo, Lazy<XDocument>>> _xmlSources;
|
|
|
|
|
|
2022-05-02 19:38:33 +02:00
|
|
|
[Obsolete("Use ctor with all params. This will be removed in Umbraco 12")]
|
|
|
|
|
public LocalizedTextServiceFileSources(
|
|
|
|
|
ILogger<LocalizedTextServiceFileSources> logger,
|
|
|
|
|
AppCaches appCaches,
|
|
|
|
|
DirectoryInfo fileSourceFolder,
|
|
|
|
|
IEnumerable<LocalizedTextServiceSupplementaryFileSource> supplementFileSources)
|
|
|
|
|
:this(
|
|
|
|
|
logger,
|
|
|
|
|
appCaches,
|
|
|
|
|
fileSourceFolder,
|
|
|
|
|
supplementFileSources, new NotFoundDirectoryContents())
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-15 17:13:34 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// This is used to configure the file sources with the main file sources shipped with Umbraco and also including supplemental/plugin based
|
2017-07-20 11:21:28 +02:00
|
|
|
/// localization files. The supplemental files will be loaded in and merged in after the primary files.
|
2015-06-15 17:13:34 +02:00
|
|
|
/// The supplemental files must be named with the 4 letter culture name with a hyphen such as : en-AU.xml
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="logger"></param>
|
|
|
|
|
/// <param name="cache"></param>
|
|
|
|
|
/// <param name="fileSourceFolder"></param>
|
|
|
|
|
/// <param name="supplementFileSources"></param>
|
|
|
|
|
public LocalizedTextServiceFileSources(
|
2020-09-21 08:19:26 +02:00
|
|
|
ILogger<LocalizedTextServiceFileSources> logger,
|
2019-01-17 11:19:06 +01:00
|
|
|
AppCaches appCaches,
|
2015-06-15 17:13:34 +02:00
|
|
|
DirectoryInfo fileSourceFolder,
|
2022-05-02 19:38:33 +02:00
|
|
|
IEnumerable<LocalizedTextServiceSupplementaryFileSource> supplementFileSources,
|
|
|
|
|
IDirectoryContents directoryContents
|
|
|
|
|
)
|
2014-12-18 12:45:04 +11:00
|
|
|
{
|
2015-06-15 17:13:34 +02:00
|
|
|
if (logger == null) throw new ArgumentNullException("logger");
|
2019-01-17 11:19:06 +01:00
|
|
|
if (appCaches == null) throw new ArgumentNullException("cache");
|
2014-12-18 12:45:04 +11:00
|
|
|
if (fileSourceFolder == null) throw new ArgumentNullException("fileSourceFolder");
|
2015-03-20 18:15:31 +11:00
|
|
|
|
2015-06-15 17:13:34 +02:00
|
|
|
_logger = logger;
|
2022-05-02 19:38:33 +02:00
|
|
|
_directoryContents = directoryContents;
|
2019-01-17 11:19:06 +01:00
|
|
|
_cache = appCaches.RuntimeCache;
|
2022-05-02 19:38:33 +02:00
|
|
|
_fileSourceFolder = fileSourceFolder;
|
|
|
|
|
_supplementFileSources = supplementFileSources;
|
2020-05-25 14:50:51 +02:00
|
|
|
|
2015-03-20 18:15:31 +11:00
|
|
|
//Create the lazy source for the _xmlSources
|
|
|
|
|
_xmlSources = new Lazy<Dictionary<CultureInfo, Lazy<XDocument>>>(() =>
|
2015-01-07 10:39:00 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
var result = new Dictionary<CultureInfo, Lazy<XDocument>>();
|
2015-01-07 10:39:00 +11:00
|
|
|
|
|
|
|
|
|
2022-05-02 19:38:33 +02:00
|
|
|
var files = GetLanguageFiles();
|
|
|
|
|
|
|
|
|
|
if (!files.Any())
|
|
|
|
|
{
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var fileInfo in files)
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
var localCopy = fileInfo;
|
2022-05-02 19:38:33 +02:00
|
|
|
var filename = Path.GetFileNameWithoutExtension(localCopy.Name).Replace("_", "-");
|
2015-03-20 18:15:31 +11:00
|
|
|
|
2019-01-27 01:17:32 -05:00
|
|
|
// TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct
|
2019-02-06 15:05:56 +01:00
|
|
|
// names instead of storing them as 2 letters but actually having a 4 letter culture. So now, we
|
2015-03-20 18:15:31 +11:00
|
|
|
// 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!
|
2022-02-16 16:03:53 +01:00
|
|
|
CultureInfo? culture = null;
|
2015-03-20 18:15:31 +11:00
|
|
|
if (filename.Length == 2)
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
//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
|
2022-05-02 19:38:33 +02:00
|
|
|
using (var fs = fileInfo.CreateReadStream())
|
2015-03-20 18:15:31 +11:00
|
|
|
using (var reader = XmlReader.Create(fs))
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
if (reader.IsStartElement())
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
if (reader.Name == "language")
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
if (reader.MoveToAttribute("culture"))
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
var cultureVal = reader.Value;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
culture = CultureInfo.GetCultureInfo(cultureVal);
|
|
|
|
|
//add to the tracked dictionary
|
|
|
|
|
_twoLetterCultureConverter[filename] = culture;
|
|
|
|
|
}
|
|
|
|
|
catch (CultureNotFoundException)
|
|
|
|
|
{
|
2022-05-02 19:38:33 +02:00
|
|
|
_logger.LogWarning("The culture {CultureValue} found in the file {CultureFile} is not a valid culture", cultureVal, fileInfo.Name);
|
2015-03-20 18:15:31 +11:00
|
|
|
//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.
|
|
|
|
|
}
|
2015-03-06 16:01:49 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-03-20 18:15:31 +11:00
|
|
|
if (culture == null)
|
2014-12-18 12:45:04 +11:00
|
|
|
{
|
2015-03-20 18:15:31 +11:00
|
|
|
culture = CultureInfo.GetCultureInfo(filename);
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-20 11:21:28 +02:00
|
|
|
//get the lazy value from cache
|
2015-04-27 13:40:41 +10:00
|
|
|
result[culture] = new Lazy<XDocument>(() => _cache.GetCacheItem<XDocument>(
|
2015-06-15 17:13:34 +02:00
|
|
|
string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name), () =>
|
2014-12-18 12:45:04 +11:00
|
|
|
{
|
2015-06-15 17:13:34 +02:00
|
|
|
XDocument xdoc;
|
|
|
|
|
|
|
|
|
|
//load in primary
|
2022-05-02 19:38:33 +02:00
|
|
|
using (var fs = localCopy.CreateReadStream())
|
2015-03-20 18:15:31 +11:00
|
|
|
{
|
2015-06-15 17:13:34 +02:00
|
|
|
xdoc = XDocument.Load(fs);
|
2015-03-20 18:15:31 +11:00
|
|
|
}
|
2015-06-15 17:13:34 +02:00
|
|
|
|
|
|
|
|
//load in supplementary
|
|
|
|
|
MergeSupplementaryFiles(culture, xdoc);
|
|
|
|
|
|
|
|
|
|
return xdoc;
|
2022-05-02 19:38:33 +02:00
|
|
|
}, isSliding: true, timeout: TimeSpan.FromMinutes(10))!);
|
2015-03-20 18:15:31 +11:00
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
});
|
|
|
|
|
|
2020-05-25 14:50:51 +02:00
|
|
|
|
2015-03-20 18:15:31 +11:00
|
|
|
}
|
|
|
|
|
|
2022-05-02 19:38:33 +02:00
|
|
|
private IEnumerable<IFileInfo> GetLanguageFiles()
|
|
|
|
|
{
|
|
|
|
|
var result = new List<IFileInfo>();
|
|
|
|
|
|
|
|
|
|
if (_fileSourceFolder is not null && _fileSourceFolder.Exists)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
result.AddRange(
|
|
|
|
|
new PhysicalDirectoryContents(_fileSourceFolder.FullName)
|
|
|
|
|
.Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_directoryContents.Exists)
|
|
|
|
|
{
|
|
|
|
|
result.AddRange(
|
|
|
|
|
_directoryContents
|
|
|
|
|
.Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-15 17:13:34 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Constructor
|
|
|
|
|
/// </summary>
|
2020-09-21 08:19:26 +02:00
|
|
|
public LocalizedTextServiceFileSources(ILogger<LocalizedTextServiceFileSources> logger, AppCaches appCaches, DirectoryInfo fileSourceFolder)
|
2019-01-17 11:19:06 +01:00
|
|
|
: this(logger, appCaches, fileSourceFolder, Enumerable.Empty<LocalizedTextServiceSupplementaryFileSource>())
|
|
|
|
|
{ }
|
2015-06-15 17:13:34 +02:00
|
|
|
|
2015-03-20 18:15:31 +11:00
|
|
|
/// <summary>
|
|
|
|
|
/// returns all xml sources for all culture files found in the folder
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public IDictionary<CultureInfo, Lazy<XDocument>> GetXmlSources()
|
|
|
|
|
{
|
|
|
|
|
return _xmlSources.Value;
|
2014-12-18 12:45:04 +11:00
|
|
|
}
|
2015-03-06 16:01:49 +11:00
|
|
|
|
2019-01-27 01:17:32 -05:00
|
|
|
// 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 :(
|
2022-02-16 16:03:53 +01:00
|
|
|
public Attempt<CultureInfo?> TryConvert2LetterCultureTo4Letter(string twoLetterCulture)
|
2015-03-06 16:01:49 +11:00
|
|
|
{
|
2022-02-16 16:03:53 +01:00
|
|
|
if (twoLetterCulture.Length != 2) return Attempt<CultureInfo?>.Fail();
|
2015-03-06 16:01:49 +11:00
|
|
|
|
2015-03-20 18:15:31 +11:00
|
|
|
//This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
|
|
|
|
|
var resolved = _xmlSources.Value;
|
|
|
|
|
|
2015-03-06 16:01:49 +11:00
|
|
|
return _twoLetterCultureConverter.ContainsKey(twoLetterCulture)
|
|
|
|
|
? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture])
|
2022-02-16 16:03:53 +01:00
|
|
|
: Attempt<CultureInfo?>.Fail();
|
2015-03-06 16:01:49 +11:00
|
|
|
}
|
2015-06-15 17:13:34 +02:00
|
|
|
|
2019-01-27 01:17:32 -05:00
|
|
|
// 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 :(
|
2022-02-16 16:03:53 +01:00
|
|
|
public Attempt<string?> TryConvert4LetterCultureTo2Letter(CultureInfo culture)
|
2015-08-20 16:07:46 +02:00
|
|
|
{
|
|
|
|
|
if (culture == null) throw new ArgumentNullException("culture");
|
|
|
|
|
|
|
|
|
|
//This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized
|
|
|
|
|
var resolved = _xmlSources.Value;
|
|
|
|
|
|
|
|
|
|
return _twoLetterCultureConverter.Values.Contains(culture)
|
|
|
|
|
? Attempt.Succeed(culture.Name.Substring(0, 2))
|
2022-02-16 16:03:53 +01:00
|
|
|
: Attempt<string?>.Fail();
|
2015-08-20 16:07:46 +02:00
|
|
|
}
|
|
|
|
|
|
2015-06-15 17:13:34 +02:00
|
|
|
private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc)
|
|
|
|
|
{
|
|
|
|
|
if (xMasterDoc.Root == null) return;
|
|
|
|
|
if (_supplementFileSources != null)
|
|
|
|
|
{
|
2019-01-22 18:03:39 -05:00
|
|
|
//now load in supplementary
|
2015-06-15 17:13:34 +02:00
|
|
|
var found = _supplementFileSources.Where(x =>
|
|
|
|
|
{
|
2019-03-20 23:44:29 +01:00
|
|
|
var extension = Path.GetExtension(x.File.FullName);
|
|
|
|
|
var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-").Replace(".user", "");
|
|
|
|
|
return extension.InvariantEquals(".xml") && (
|
|
|
|
|
fileCultureName.InvariantEquals(culture.Name)
|
|
|
|
|
|| fileCultureName.InvariantEquals(culture.TwoLetterISOLanguageName)
|
|
|
|
|
);
|
2015-06-15 17:13:34 +02:00
|
|
|
});
|
2017-07-20 11:21:28 +02:00
|
|
|
|
2015-06-15 17:13:34 +02:00
|
|
|
foreach (var supplementaryFile in found)
|
|
|
|
|
{
|
|
|
|
|
using (var fs = supplementaryFile.File.OpenRead())
|
|
|
|
|
{
|
|
|
|
|
XDocument xChildDoc;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
xChildDoc = XDocument.Load(fs);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2020-09-16 09:40:49 +02:00
|
|
|
_logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName);
|
2015-06-15 17:13:34 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-20 23:44:29 +01:00
|
|
|
if (xChildDoc.Root == null || xChildDoc.Root.Name != "language") continue;
|
2015-06-15 17:13:34 +02:00
|
|
|
foreach (var xArea in xChildDoc.Root.Elements("area")
|
2022-02-16 16:03:53 +01:00
|
|
|
.Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
|
2015-06-15 17:13:34 +02:00
|
|
|
{
|
2022-02-16 16:03:53 +01:00
|
|
|
var areaAlias = (string)xArea.Attribute("alias")!;
|
2015-06-15 17:13:34 +02:00
|
|
|
|
2022-02-16 16:03:53 +01:00
|
|
|
var areaFound = xMasterDoc.Root.Elements("area").FirstOrDefault(x => ((string)x.Attribute("alias")!) == areaAlias);
|
2015-06-15 17:13:34 +02:00
|
|
|
if (areaFound == null)
|
|
|
|
|
{
|
|
|
|
|
//add the whole thing
|
|
|
|
|
xMasterDoc.Root.Add(xArea);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
MergeChildKeys(xArea, areaFound, supplementaryFile.OverwriteCoreKeys);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MergeChildKeys(XElement source, XElement destination, bool overwrite)
|
|
|
|
|
{
|
|
|
|
|
if (destination == null) throw new ArgumentNullException("destination");
|
|
|
|
|
if (source == null) throw new ArgumentNullException("source");
|
|
|
|
|
|
|
|
|
|
//merge in the child elements
|
|
|
|
|
foreach (var key in source.Elements("key")
|
2022-02-16 16:03:53 +01:00
|
|
|
.Where(x => ((string)x.Attribute("alias")!).IsNullOrWhiteSpace() == false))
|
2015-06-15 17:13:34 +02:00
|
|
|
{
|
2022-02-16 16:03:53 +01:00
|
|
|
var keyAlias = (string)key.Attribute("alias")!;
|
|
|
|
|
var keyFound = destination.Elements("key").FirstOrDefault(x => ((string)x.Attribute("alias")!) == keyAlias);
|
2015-06-15 17:13:34 +02:00
|
|
|
if (keyFound == null)
|
|
|
|
|
{
|
|
|
|
|
//append, it doesn't exist
|
|
|
|
|
destination.Add(key);
|
|
|
|
|
}
|
|
|
|
|
else if (overwrite)
|
|
|
|
|
{
|
|
|
|
|
//overwrite
|
|
|
|
|
keyFound.Value = key.Value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-12-18 12:45:04 +11:00
|
|
|
}
|
2017-07-20 11:21:28 +02:00
|
|
|
}
|