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; }