diff --git a/src/Microsoft.TemplateEngine.Abstractions/IFileLocalizationModel.cs b/src/Microsoft.TemplateEngine.Abstractions/IFileLocalizationModel.cs new file mode 100644 index 00000000000..2ce6044c819 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Abstractions/IFileLocalizationModel.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.TemplateEngine.Abstractions +{ + /// + /// Model type that contains the string-replace operations to be performed + /// on a file in order to localize it. + /// + public interface IFileLocalizationModel + { + /// + /// Gets the globbing pattern to determine the files + /// that these localizations will be applied to. + /// + string File { get; } + + /// + /// Gets the dictionary containing the localized strings as values + /// where the keys are the string to be replaced. + /// + IReadOnlyDictionary Localizations { get; } + } +} diff --git a/src/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt b/src/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt index 5f282702bb0..be52bd86aa0 100644 --- a/src/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ - \ No newline at end of file +Microsoft.TemplateEngine.Abstractions.IFileLocalizationModel +~Microsoft.TemplateEngine.Abstractions.IFileLocalizationModel.File.get -> string +~Microsoft.TemplateEngine.Abstractions.IFileLocalizationModel.Localizations.get -> System.Collections.Generic.IReadOnlyDictionary \ No newline at end of file diff --git a/src/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs b/src/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs index 88fa4d83479..8d95da9ff9b 100644 --- a/src/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs +++ b/src/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs @@ -19,6 +19,8 @@ public interface IGlobalRunSpec IReadOnlyList> Special { get; } + IReadOnlyDictionary> LocalizationOperations { get; } + IReadOnlyList IgnoreFileNames { get; } bool TryGetTargetRelPath(string sourceRelPath, out string targetRelPath); diff --git a/src/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt b/src/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt index 5f282702bb0..24574c3fe2b 100644 --- a/src/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt @@ -1 +1 @@ - \ No newline at end of file +~Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.LocalizationOperations.get -> System.Collections.Generic.IReadOnlyDictionary> \ No newline at end of file diff --git a/src/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs b/src/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs index 072d68516b6..6c07fbfa653 100644 --- a/src/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs +++ b/src/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs @@ -8,6 +8,7 @@ using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.Mount; using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; using Microsoft.TemplateEngine.Utils; namespace Microsoft.TemplateEngine.Core.Util @@ -252,7 +253,8 @@ private void RunInternal(IEngineEnvironmentSettings environmentSettings, IDirect } else if (!copy) { - ProcessFile(file, sourceRel, targetDir, spec, fallback, fileGlobProcessors); + spec.LocalizationOperations.TryGetValue(sourceRel, out var localizedReplacements); + ProcessFile(file, sourceRel, targetDir, spec, fallback, localizedReplacements, fileGlobProcessors); } else { @@ -272,7 +274,14 @@ private void RunInternal(IEngineEnvironmentSettings environmentSettings, IDirect } } - private void ProcessFile(IFile sourceFile, string sourceRel, string targetDir, IGlobalRunSpec spec, IProcessor fallback, IEnumerable> fileGlobProcessors) + private void ProcessFile( + IFile sourceFile, + string sourceRel, + string targetDir, + IGlobalRunSpec spec, + IProcessor fallback, + IReadOnlyDictionary localizedReplacementsForFile, + IEnumerable> fileGlobProcessors) { IProcessor runner = fileGlobProcessors.FirstOrDefault(x => x.Key.IsMatch(sourceRel)).Value ?? fallback; if (runner == null) @@ -280,6 +289,15 @@ private void ProcessFile(IFile sourceFile, string sourceRel, string targetDir, I throw new InvalidOperationException("At least one of [runner] or [fallback] cannot be null"); } + if (localizedReplacementsForFile != null) + { + List fileLocalizationOperations = localizedReplacementsForFile + .Select(l => new Replacement(l.Key.TokenConfig(), l.Value, null, true)) + .Cast() + .ToList(); + runner = runner.CloneAndAppendOperations(fileLocalizationOperations); + } + if (!spec.TryGetTargetRelPath(sourceRel, out string targetRel)) { targetRel = sourceRel; diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs index 5a051bf2b3e..f31edd22def 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs @@ -30,11 +30,13 @@ internal GlobalRunSpec( IVariableCollection variables, IGlobalRunConfig globalConfig, IReadOnlyList> fileGlobConfigs, + IReadOnlyDictionary> localizationOperations, IReadOnlyList ignoreFileNames) { EnsureOperationConfigs(componentManager); RootVariableCollection = variables; + LocalizationOperations = localizationOperations; IgnoreFileNames = ignoreFileNames; Operations = ResolveOperations(globalConfig, templateRoot, variables, parameters); List> specials = new List>(); @@ -72,6 +74,8 @@ internal GlobalRunSpec( public IReadOnlyList> Special { get; } + public IReadOnlyDictionary> LocalizationOperations { get; } + public IReadOnlyList IgnoreFileNames { get; } internal IReadOnlyDictionary Rename { get; private set; } diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ILocalizationModel.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ILocalizationModel.cs index b63d5892c01..370d377814e 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ILocalizationModel.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ILocalizationModel.cs @@ -38,5 +38,10 @@ internal interface ILocalizationModel /// The keys represent the id of the post actions. /// IReadOnlyDictionary PostActions { get; } + + /// + /// Gets the localizated string replacements to be applied to contents of files. + /// + IReadOnlyList LocalizableReplacements { get; } } } diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs index f0a3ef61366..d4fd349ae31 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs @@ -14,6 +14,8 @@ internal interface IRunnableProjectConfig IReadOnlyList> SpecialOperationConfig { get; } + IReadOnlyDictionary> LocalizationOperations { get; } + IGlobalRunConfig OperationConfig { get; } IReadOnlyList Sources { get; } diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs index 7224bf3a91b..80050698594 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs @@ -16,13 +16,15 @@ public LocalizationModel( string? description, string? author, IReadOnlyDictionary parameterSymbols, - IReadOnlyDictionary postActions) + IReadOnlyDictionary postActions, + IReadOnlyList fileLocalizations) { Name = name; Description = description; Author = author; ParameterSymbols = parameterSymbols ?? throw new ArgumentNullException(nameof(parameterSymbols)); PostActions = postActions ?? throw new ArgumentNullException(nameof(postActions)); + LocalizableReplacements = fileLocalizations ?? throw new ArgumentNullException(nameof(fileLocalizations)); } /// @@ -39,5 +41,8 @@ public LocalizationModel( /// public IReadOnlyDictionary PostActions { get; } + + /// + public IReadOnlyList LocalizableReplacements { get; } } } diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs index 0baa48babe9..4df896e78de 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs @@ -41,12 +41,15 @@ public static ILocalizationModel Deserialize(IFile file) var symbols = LoadSymbolModels(localizedStrings); var postActions = LoadPostActionModels(localizedStrings); + // TODO load localized replacements: "localizedReplacements/globbing_string/symbol_name": "localized value" + return new LocalizationModel( name: localizedStrings.FirstOrDefault(s => s.Key == "name").Value, description: localizedStrings.FirstOrDefault(s => s.Key == "description").Value, author: localizedStrings.FirstOrDefault(s => s.Key == "author").Value, symbols, - postActions); + postActions, + pass_localized_replacements_here); } /// diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/RunnableProjectGenerator.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/RunnableProjectGenerator.cs index b28ff6f4e3d..e783c1377f6 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/RunnableProjectGenerator.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/RunnableProjectGenerator.cs @@ -59,7 +59,7 @@ public Task CreateAsync( IOrchestrator2 basicOrchestrator = new Core.Util.Orchestrator(); RunnableProjectOrchestrator orchestrator = new RunnableProjectOrchestrator(basicOrchestrator); - GlobalRunSpec runSpec = new GlobalRunSpec(templateData.TemplateSourceRoot, environmentSettings.Components, parameters, variables, template.Config.OperationConfig, template.Config.SpecialOperationConfig, template.Config.IgnoreFileNames); + GlobalRunSpec runSpec = new GlobalRunSpec(templateData.TemplateSourceRoot, environmentSettings.Components, parameters, variables, template.Config.OperationConfig, template.Config.SpecialOperationConfig, template.Config.LocalizationOperations, template.Config.IgnoreFileNames); foreach (FileSourceMatchInfo source in template.Config.Sources) { @@ -96,7 +96,7 @@ public Task GetCreationEffectsAsync( IOrchestrator2 basicOrchestrator = new Core.Util.Orchestrator(); RunnableProjectOrchestrator orchestrator = new RunnableProjectOrchestrator(basicOrchestrator); - GlobalRunSpec runSpec = new GlobalRunSpec(templateData.TemplateSourceRoot, environmentSettings.Components, parameters, variables, template.Config.OperationConfig, template.Config.SpecialOperationConfig, template.Config.IgnoreFileNames); + GlobalRunSpec runSpec = new GlobalRunSpec(templateData.TemplateSourceRoot, environmentSettings.Components, parameters, variables, template.Config.OperationConfig, template.Config.SpecialOperationConfig, template.Config.LocalizationOperations, template.Config.IgnoreFileNames); List changes = new List(); foreach (FileSourceMatchInfo source in template.Config.Sources) diff --git a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/SimpleConfigModel.cs b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/SimpleConfigModel.cs index 155b191f243..69bc6515c78 100644 --- a/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/SimpleConfigModel.cs +++ b/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/SimpleConfigModel.cs @@ -102,7 +102,7 @@ internal SimpleConfigModel(IEngineEnvironmentSettings environmentSettings, JObje src.Exclude = item.Get(nameof(src.Exclude)); src.Include = item.Get(nameof(src.Include)); src.Condition = item.ToString(nameof(src.Condition)); - src.Rename = item.Get(nameof(src.Rename)).ToStringDictionary().ToDictionary(x => x.Key, x => x.Value); + src.Rename = item.Get(nameof(src.Rename)).ToStringDictionary(); List modifiers = new List(); src.Modifiers = modifiers; @@ -183,7 +183,7 @@ internal SimpleConfigModel(IEngineEnvironmentSettings environmentSettings, JObje } } - _postActions = RunnableProjects.PostActionModel.LoadListFromJArray(source.Get("PostActions"), _logger, filename); + _postActions = PostActionModel.LoadListFromJArray(source.Get("PostActions"), _logger, filename); PrimaryOutputs = CreationPathModel.ListFromJArray(source.Get(nameof(PrimaryOutputs))); // Custom operations at the global level @@ -204,6 +204,18 @@ internal SimpleConfigModel(IEngineEnvironmentSettings environmentSettings, JObje } _specialCustomSetup = specialCustomSetup; + + // Localized string replacements. Template config file only contains the authoring language replacements. The rest will come from localization files. + Dictionary localizedReplacementFiles = source.ToJTokenDictionary(StringComparer.OrdinalIgnoreCase, "localizedReplacements"); + Dictionary> localizations = new Dictionary>(); + if (localizedReplacementFiles != null) + { + foreach (var fileTokenPair in localizedReplacementFiles) + { + localizations.Add(fileTokenPair.Key, fileTokenPair.Value.ToStringDictionary(StringComparer.OrdinalIgnoreCase)); + } + } + LocalizationOperations = localizations; } internal SimpleConfigModel(IFile templateFile, ISimpleConfigModifiers configModifiers = null) @@ -254,6 +266,14 @@ public IReadOnlyList IgnoreFileNames } } + /// + /// Gets the localized replacement strings for each file. + /// Each key of the outter dictionary is a globbing pattern that defines which files + /// will be considered for replacement. Each value is a dictionary mapping the + /// token to be replaced to the new value. + /// + public IReadOnlyDictionary> LocalizationOperations { get; private set; } + IReadOnlyList IRunnableProjectConfig.Classifications => Classifications; IReadOnlyList IRunnableProjectConfig.Sources @@ -652,6 +672,40 @@ internal void Localize(ILocalizationModel locModel) postAction.Localize(postActionLocModel, _logger); } } + + if (locModel.LocalizableReplacements != null) + { + // Recreate the data in the localized way. + Dictionary> newLocalizedOperations = new Dictionary>(); + + // Get the localized replacements from the model. + foreach (var fileLocalizations in locModel.LocalizableReplacements) + { + // Find the matching file listed from the template config file + if (!LocalizationOperations.TryGetValue(fileLocalizations.File, out var replacements)) + { + // No data available in template config file for this localized data. + continue; + } + + Dictionary localizedReplacementsForFile = new Dictionary(); + foreach (var replacement in replacements) + { + // Find the matching localization for the replacement + if (!fileLocalizations.Localizations.TryGetValue(replacement.Key, out string localizedString)) + { + // No loc data for this token. Use original value. + localizedReplacementsForFile[replacement.Key] = replacement.Value; + continue; + } + + localizedReplacementsForFile[replacement.Key] = localizedString; + } + newLocalizedOperations[fileLocalizations.File] = localizedReplacementsForFile; + } + + LocalizationOperations = newLocalizedOperations; + } } /// diff --git a/src/Shared/JExtensions.cs b/src/Shared/JExtensions.cs index 596b48c7393..93734e5dee1 100644 --- a/src/Shared/JExtensions.cs +++ b/src/Shared/JExtensions.cs @@ -181,7 +181,7 @@ internal static IEnumerable PropertiesOf(this JToken? token, string? return res as T; } - internal static IReadOnlyDictionary ToStringDictionary(this JToken token, StringComparer? comparer = null, string? propertyName = null) + internal static Dictionary ToStringDictionary(this JToken token, StringComparer? comparer = null, string? propertyName = null) { Dictionary result = new Dictionary(comparer ?? StringComparer.Ordinal); @@ -199,7 +199,7 @@ internal static IReadOnlyDictionary ToStringDictionary(this JTok } // Leaves the values as JTokens. - internal static IReadOnlyDictionary ToJTokenDictionary(this JToken token, StringComparer? comparaer = null, string? propertyName = null) + internal static Dictionary ToJTokenDictionary(this JToken token, StringComparer? comparaer = null, string? propertyName = null) { Dictionary result = new Dictionary(comparaer ?? StringComparer.Ordinal); diff --git a/test/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.cs b/test/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.cs index 37f2ae20583..001e6dad198 100644 --- a/test/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.cs +++ b/test/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.cs @@ -15,7 +15,7 @@ public MockGlobalRunSpec() CopyOnly = new List(); Operations = new List(); Special = new List>(); - LocalizationOperations = new Dictionary>(); + LocalizationOperations = new Dictionary>(); Rename = new Dictionary(); IgnoreFileNames = new[] { "-.-", "_._" }; } @@ -32,7 +32,7 @@ public MockGlobalRunSpec() public IReadOnlyList> Special { get; set; } - public IReadOnlyDictionary> LocalizationOperations { get; set; } + public IReadOnlyDictionary> LocalizationOperations { get; set; } public IReadOnlyList IgnoreFileNames { get; set; }