diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index 26a2e9fb60ca..ea75f369afa1 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.FileProviders; @@ -166,7 +167,7 @@ public LocalizedTextServiceFileSources(ILogger } /// - /// returns all xml sources for all culture files found in the folder + /// Returns all xml sources for all culture files found in the folder. /// /// public IDictionary> GetXmlSources() => _xmlSources.Value; @@ -179,7 +180,15 @@ private IEnumerable GetLanguageFiles() { result.AddRange( new PhysicalDirectoryContents(_fileSourceFolder.FullName) - .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))); + .Where(x => !x.IsDirectory && !x.Name.Contains("user") && x.Name.EndsWith(".xml"))); // Filter out *.user.xml + } + + if (_supplementFileSources is not null) + { + // Get only the .xml files and filter out the user defined language files (*.user.xml) that overwrite the default + result.AddRange(_supplementFileSources + .Where(x => !x.FileInfo.Name.Contains("user") && x.FileInfo.Name.EndsWith(".xml")) + .Select(x => x.FileInfo)); } if (_directoryContents.Exists) @@ -236,8 +245,8 @@ private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc) // now load in supplementary IEnumerable found = _supplementFileSources.Where(x => { - var extension = Path.GetExtension(x.File.FullName); - var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-") + var extension = Path.GetExtension(x.FileInfo.Name); + var fileCultureName = Path.GetFileNameWithoutExtension(x.FileInfo.Name).Replace("_", "-") .Replace(".user", string.Empty); return extension.InvariantEquals(".xml") && ( fileCultureName.InvariantEquals(culture.Name) @@ -246,16 +255,16 @@ private void MergeSupplementaryFiles(CultureInfo culture, XDocument xMasterDoc) foreach (LocalizedTextServiceSupplementaryFileSource supplementaryFile in found) { - using (FileStream fs = supplementaryFile.File.OpenRead()) + using (Stream stream = supplementaryFile.FileInfo.CreateReadStream()) { XDocument xChildDoc; try { - xChildDoc = XDocument.Load(fs); + xChildDoc = XDocument.Load(stream); } catch (Exception ex) { - _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName); + _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.FileInfo.Name); continue; } diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs index cff9a55234b9..3ada83dc3c27 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs @@ -1,14 +1,27 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; + namespace Umbraco.Cms.Core.Services; public class LocalizedTextServiceSupplementaryFileSource { + [Obsolete("Use other ctor. Will be removed in Umbraco 12")] public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys) + : this(new PhysicalFileInfo(file), overwriteCoreKeys) { - File = file ?? throw new ArgumentNullException("file"); + } + + public LocalizedTextServiceSupplementaryFileSource(IFileInfo file, bool overwriteCoreKeys) + { + FileInfo = file ?? throw new ArgumentNullException(nameof(file)); + File = file is PhysicalFileInfo ? new FileInfo(file.PhysicalPath) : null!; OverwriteCoreKeys = overwriteCoreKeys; } + [Obsolete("Use FileInfo instead. Will be removed in Umbraco 12")] public FileInfo File { get; } + public IFileInfo FileInfo { get; } + public bool OverwriteCoreKeys { get; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index dd5b77abec5f..c9208b5bdc78 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -102,7 +102,14 @@ private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSou IServiceProvider container) { IHostingEnvironment hostingEnvironment = container.GetRequiredService(); + + // TODO: (for >= v13) Rethink whether all language files (.xml and .user.xml) should be located in ~/config/lang + // instead of ~/umbraco/config/lang and ~/config/lang. + // Currently when extending Umbraco, a new language file that the backoffice will be available in, should be placed + // in ~/umbraco/config/lang, while 'user' translation files for overrides are in ~/config/lang (according to our docs). + // Such change will be breaking and we would need to document this clearly. var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); + var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); return new LocalizedTextServiceFileSources( diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs index 54e25240e0e7..60a946a553d0 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs @@ -1,6 +1,8 @@ +using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; @@ -42,20 +44,30 @@ private static IEnumerable GetSuppl // gets all langs files in /app_plugins real or virtual locations IEnumerable pluginLangFileSources = GetPluginLanguageFileSources(webFileProvider, Cms.Core.Constants.SystemDirectories.AppPlugins, false); - // user defined langs that overwrite the default, these should not be used by plugin creators + // user defined language files that overwrite the default, these should not be used by plugin creators var userConfigLangFolder = Cms.Core.Constants.SystemDirectories.Config .TrimStart(Cms.Core.Constants.CharArrays.Tilde); - IEnumerable userLangFileSources = contentFileProvider.GetDirectoryContents(userConfigLangFolder) - .Where(x => x.IsDirectory && x.Name.InvariantEquals("lang")) - .Select(x => new DirectoryInfo(x.PhysicalPath)) - .SelectMany(x => x.GetFiles("*.user.xml", SearchOption.TopDirectoryOnly)) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); + var configLangFileSources = new List(); + + foreach (IFileInfo langFileSource in contentFileProvider.GetDirectoryContents(userConfigLangFolder)) + { + if (langFileSource.IsDirectory && langFileSource.Name.InvariantEquals("lang")) + { + foreach (IFileInfo langFile in contentFileProvider.GetDirectoryContents($"{userConfigLangFolder}/{langFileSource.Name}")) + { + if (langFile.Name.InvariantEndsWith(".xml") && langFile.PhysicalPath is not null) + { + configLangFileSources.Add(new LocalizedTextServiceSupplementaryFileSource(langFile, true)); + } + } + } + } return localPluginFileSources .Concat(pluginLangFileSources) - .Concat(userLangFileSources); + .Concat(configLangFileSources); } @@ -83,13 +95,12 @@ private static IEnumerable GetPlugi foreach (var langFolder in GetLangFolderPaths(fileProvider, pluginFolderPath)) { // request all the files out of the path, these will have physicalPath set. - IEnumerable localizationFiles = fileProvider + IEnumerable localizationFiles = fileProvider .GetDirectoryContents(langFolder) .Where(x => !string.IsNullOrEmpty(x.PhysicalPath)) - .Where(x => x.Name.InvariantEndsWith(".xml")) - .Select(x => new FileInfo(x.PhysicalPath)); + .Where(x => x.Name.InvariantEndsWith(".xml")); - foreach (FileInfo file in localizationFiles) + foreach (IFileInfo file in localizationFiles) { yield return new LocalizedTextServiceSupplementaryFileSource(file, overwriteCoreKeys); }