From 1f1cac71678e7fc67ff757236fdecf4e73502655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 6 Dec 2024 23:36:09 +0100 Subject: [PATCH 01/32] Command line refactoring. --- .../GetCliOptionsResult.cs | 8 +++ src/OrchardCoreContrib.PoExtractor/Program.cs | 52 +++++++++---------- 2 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs diff --git a/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs b/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs new file mode 100644 index 0000000..7d8086d --- /dev/null +++ b/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs @@ -0,0 +1,8 @@ +namespace OrchardCoreContrib.PoExtractor; + +public class GetCliOptionsResult +{ + public string Language { get; set; } + public string TemplateEngine { get; set; } + public string SingleOutputFile { get; set; } +} \ No newline at end of file diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index 45b6e03..1832b6b 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -3,10 +3,6 @@ using OrchardCoreContrib.PoExtractor.DotNet.VB; using OrchardCoreContrib.PoExtractor.Liquid; using OrchardCoreContrib.PoExtractor.Razor; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; namespace OrchardCoreContrib.PoExtractor; @@ -34,9 +30,9 @@ public static void Main(string[] args) return; } - (string language, string templateEngine, string singleOutputFile) = GetCliOptions(args); + var options = GetCliOptions(args); - if (language == null || templateEngine == null) + if (options.Language == null || options.TemplateEngine == null) { ShowHelp(); @@ -46,7 +42,7 @@ public static void Main(string[] args) var projectFiles = new List(); var projectProcessors = new List(); - if (language == Language.CSharp) + if (options.Language == Language.CSharp) { projectProcessors.Add(new CSharpProjectProcessor()); @@ -63,21 +59,21 @@ public static void Main(string[] args) .OrderBy(f => f)); } - if (templateEngine == TemplateEngine.Both) + if (options.TemplateEngine == TemplateEngine.Both) { projectProcessors.Add(new RazorProjectProcessor()); projectProcessors.Add(new LiquidProjectProcessor()); } - else if (templateEngine == TemplateEngine.Razor) + else if (options.TemplateEngine == TemplateEngine.Razor) { projectProcessors.Add(new RazorProjectProcessor()); } - else if (templateEngine == TemplateEngine.Liquid) + else if (options.TemplateEngine == TemplateEngine.Liquid) { projectProcessors.Add(new LiquidProjectProcessor()); } - var isSingleFileOutput = !string.IsNullOrEmpty(singleOutputFile); + var isSingleFileOutput = !string.IsNullOrEmpty(options.SingleOutputFile); var localizableStrings = new LocalizableStringCollection(); foreach (var projectFile in projectFiles) { @@ -116,7 +112,7 @@ public static void Main(string[] args) { if (localizableStrings.Values.Any()) { - var potPath = Path.Combine(outputPath, singleOutputFile); + var potPath = Path.Combine(outputPath, options.SingleOutputFile); Directory.CreateDirectory(Path.GetDirectoryName(potPath)); @@ -128,11 +124,15 @@ public static void Main(string[] args) } } - private static (string language, string templateEngine, string singleOutputFile) GetCliOptions(string[] args) + private static GetCliOptionsResult GetCliOptions(string[] args) { - var language = _defaultLanguage; - var templateEngine = _defaultTemplateEngine; - string singleOutputFile = null; + var result = new GetCliOptionsResult + { + Language = _defaultLanguage, + TemplateEngine = _defaultTemplateEngine, + SingleOutputFile = null, + }; + for (int i = 4; i <= args.Length; i += 2) { switch (args[i - 2]) @@ -141,15 +141,15 @@ private static (string language, string templateEngine, string singleOutputFile) case "--language": if (args[i - 1].Equals(Language.CSharp, StringComparison.CurrentCultureIgnoreCase)) { - language = Language.CSharp; + result.Language = Language.CSharp; } else if (args[i - 1].Equals(Language.VisualBasic, StringComparison.CurrentCultureIgnoreCase)) { - language = Language.VisualBasic; + result.Language = Language.VisualBasic; } else { - language = null; + result.Language = null; } break; @@ -157,15 +157,15 @@ private static (string language, string templateEngine, string singleOutputFile) case "--template": if (args[i - 1].Equals(TemplateEngine.Razor, StringComparison.CurrentCultureIgnoreCase)) { - templateEngine = TemplateEngine.Razor; + result.TemplateEngine = TemplateEngine.Razor; } else if (args[i - 1].Equals(TemplateEngine.Liquid, StringComparison.CurrentCultureIgnoreCase)) { - templateEngine = TemplateEngine.Liquid; + result.TemplateEngine = TemplateEngine.Liquid; } else { - templateEngine = null; + result.TemplateEngine = null; } break; @@ -195,18 +195,18 @@ private static (string language, string templateEngine, string singleOutputFile) case "--single": if (!string.IsNullOrEmpty(args[i - 1])) { - singleOutputFile = args[i - 1]; + result.SingleOutputFile = args[i - 1]; } break; default: - language = null; - templateEngine = null; + result.Language = null; + result.TemplateEngine = null; break; } } - return (language, templateEngine, singleOutputFile); + return result; } private static void ShowHelp() From 9eb4f21e0b4fe7c6884dd4cb3596c2c3da2aa997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 6 Dec 2024 23:40:31 +0100 Subject: [PATCH 02/32] Add -p/--plugin command line option. --- .../GetCliOptionsResult.cs | 1 + src/OrchardCoreContrib.PoExtractor/Program.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs b/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs index 7d8086d..fa1f5a1 100644 --- a/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs +++ b/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs @@ -5,4 +5,5 @@ public class GetCliOptionsResult public string Language { get; set; } public string TemplateEngine { get; set; } public string SingleOutputFile { get; set; } + public IList Plugins { get; set; } = new List(); } \ No newline at end of file diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index 1832b6b..b3aa283 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -198,6 +198,14 @@ private static GetCliOptionsResult GetCliOptions(string[] args) result.SingleOutputFile = args[i - 1]; } + break; + case "-p": + case "--plugin": + if (File.Exists(args[i - 1])) + { + result.Plugins.Add(args[i - 1]); + } + break; default: result.Language = null; @@ -226,5 +234,7 @@ private static void ShowHelp() Console.WriteLine(" -i, --ignore project1,project2 Ignores extracting PO filed from a given project(s)."); Console.WriteLine(" --localizer localizer1,localizer2 Specifies the name of the localizer(s) that will be used during the extraction process."); Console.WriteLine(" -s, --single Specifies the single output file."); + Console.WriteLine(" -p, --plugin A path to a C# script file which can define further IProjectProcessor"); + Console.WriteLine(" implementations. You can have multiple of this switch in a call."); } } From a8d4f00c488befdf7aa4703187d1c2fd498c5ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Fri, 6 Dec 2024 23:44:54 +0100 Subject: [PATCH 03/32] Add Microsoft.CodeAnalysis.CSharp.Scripting package. --- Directory.Packages.props | 3 ++- .../OrchardCoreContrib.PoExtractor.csproj | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7f1df29..348e03a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,8 @@ - + + diff --git a/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj b/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj index 183c158..f6b2210 100644 --- a/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj +++ b/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj @@ -31,4 +31,7 @@ + + + From f3420df050865dd2c0f329332933ed4db2885558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 00:36:06 +0100 Subject: [PATCH 04/32] Add CSX plugin support. --- src/OrchardCoreContrib.PoExtractor/Program.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index b3aa283..3a7e11e 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -1,4 +1,6 @@ -using OrchardCoreContrib.PoExtractor.DotNet; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using OrchardCoreContrib.PoExtractor.DotNet; using OrchardCoreContrib.PoExtractor.DotNet.CS; using OrchardCoreContrib.PoExtractor.DotNet.VB; using OrchardCoreContrib.PoExtractor.Liquid; @@ -11,7 +13,7 @@ public class Program private static readonly string _defaultLanguage = Language.CSharp; private static readonly string _defaultTemplateEngine = TemplateEngine.Both; - public static void Main(string[] args) + public static async Task Main(string[] args) { if (args.Length < 2 || args.Length > 10 || args.Length % 2 == 1) { @@ -73,6 +75,11 @@ public static void Main(string[] args) projectProcessors.Add(new LiquidProjectProcessor()); } + if (options.Plugins.Count > 0) + { + await ProcessPluginsAsync(options.Plugins, projectProcessors, projectFiles); + } + var isSingleFileOutput = !string.IsNullOrEmpty(options.SingleOutputFile); var localizableStrings = new LocalizableStringCollection(); foreach (var projectFile in projectFiles) @@ -124,6 +131,21 @@ public static void Main(string[] args) } } + private static async Task ProcessPluginsAsync( + IList plugins, + List projectProcessors, + List projectFiles) + { + var options = ScriptOptions.Default + .AddReferences(typeof(IProjectProcessor).Assembly); + + foreach (var plugin in plugins) + { + var code = await File.ReadAllTextAsync(plugin); + await CSharpScript.EvaluateAsync(code, options, new { projectProcessors, projectFiles }); + } + } + private static GetCliOptionsResult GetCliOptions(string[] args) { var result = new GetCliOptionsResult @@ -234,7 +256,7 @@ private static void ShowHelp() Console.WriteLine(" -i, --ignore project1,project2 Ignores extracting PO filed from a given project(s)."); Console.WriteLine(" --localizer localizer1,localizer2 Specifies the name of the localizer(s) that will be used during the extraction process."); Console.WriteLine(" -s, --single Specifies the single output file."); - Console.WriteLine(" -p, --plugin A path to a C# script file which can define further IProjectProcessor"); + Console.WriteLine(" -p, --plugin A path to a C# script (.csx) file which can define further IProjectProcessor"); Console.WriteLine(" implementations. You can have multiple of this switch in a call."); } } From c172b43a924751c6a3a00adf8631eb422910eeed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 00:53:20 +0100 Subject: [PATCH 05/32] Some documentation. --- OrchardCoreContrib.PoExtractor.sln | 1 + README.md | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/OrchardCoreContrib.PoExtractor.sln b/OrchardCoreContrib.PoExtractor.sln index cc86e32..5b1888d 100644 --- a/OrchardCoreContrib.PoExtractor.sln +++ b/OrchardCoreContrib.PoExtractor.sln @@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props + README.md = README.md EndProjectSection EndProject Global diff --git a/README.md b/README.md index b2247f1..d47c85d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,15 @@ Specifies the code language to extracts translatable strings from. Default: `C#` Specifies the template engine to extract the translatable strings from. Default: `Razor` & `Liquid` templates. +- **`-p|--plugin {path to CSX file}`** + +Specifies a path to a C# script file which can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of this switch in one call to load several plugins at once. + +When executing the plugins, the _OrchardCoreContrib.PoExtractor.Abstractions_ library is automatically loaded, and two globals are defined: + +- `List projectProcessors`: Add an instance of your custom `IProjectProcessor` implementation type to this list. +- `List projectFiles`: In the unlikely case that you have to add a new project file type (such as _.fsproj_) add the project file paths to this list. + ## Uninstallation ```powershell From aeaa3bdb554beadeeb59ac7ba09190d8e903ea8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 03:55:33 +0100 Subject: [PATCH 06/32] Access bug fix. --- README.md | 2 +- src/OrchardCoreContrib.PoExtractor/Program.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d47c85d..d6ec4bf 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Specifies the template engine to extract the translatable strings from. Default: Specifies a path to a C# script file which can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of this switch in one call to load several plugins at once. -When executing the plugins, the _OrchardCoreContrib.PoExtractor.Abstractions_ library is automatically loaded, and two globals are defined: +When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are automatically loaded, and two globals are defined: - `List projectProcessors`: Add an instance of your custom `IProjectProcessor` implementation type to this list. - `List projectFiles`: In the unlikely case that you have to add a new project file type (such as _.fsproj_) add the project file paths to this list. diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index 3a7e11e..5bf7f01 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -136,13 +136,12 @@ private static async Task ProcessPluginsAsync( List projectProcessors, List projectFiles) { - var options = ScriptOptions.Default - .AddReferences(typeof(IProjectProcessor).Assembly); + var options = ScriptOptions.Default.AddReferences(typeof(Program).Assembly); foreach (var plugin in plugins) { var code = await File.ReadAllTextAsync(plugin); - await CSharpScript.EvaluateAsync(code, options, new { projectProcessors, projectFiles }); + await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles)); } } @@ -259,4 +258,6 @@ private static void ShowHelp() Console.WriteLine(" -p, --plugin A path to a C# script (.csx) file which can define further IProjectProcessor"); Console.WriteLine(" implementations. You can have multiple of this switch in a call."); } + + public record PluginContext(List projectProcessors, List projectFiles); } From 31224e09996a5d92a610d1a5f8314e5a80b9648b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 03:55:49 +0100 Subject: [PATCH 07/32] Add sample and test. --- src/OrchardCoreContrib.PoExtractor/Program.cs | 2 +- ...rchardCoreContrib.PoExtractor.Tests.csproj | 5 ++ .../BasicJsonLocalizationProcessor.csx | 77 +++++++++++++++++++ .../PluginTestFiles/i18n/en.json | 13 ++++ .../PluginTests.cs | 66 ++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx create mode 100644 test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json create mode 100644 test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index 5bf7f01..cafa5ca 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -131,7 +131,7 @@ public static async Task Main(string[] args) } } - private static async Task ProcessPluginsAsync( + public static async Task ProcessPluginsAsync( IList plugins, List projectProcessors, List projectFiles) diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/OrchardCoreContrib.PoExtractor.Tests.csproj b/test/OrchardCoreContrib.PoExtractor.Tests/OrchardCoreContrib.PoExtractor.Tests.csproj index f2b2700..77d8829 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/OrchardCoreContrib.PoExtractor.Tests.csproj +++ b/test/OrchardCoreContrib.PoExtractor.Tests/OrchardCoreContrib.PoExtractor.Tests.csproj @@ -15,4 +15,9 @@ + + + PreserveNewest + + diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx new file mode 100644 index 0000000..cb4f51e --- /dev/null +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json.Nodes; +using OrchardCoreContrib.PoExtractor; + +// This example plugin implements processing for a very simplistic subset of the i18next JSON format. It only supports +// strings and other objects, and the files must be located in i18n/{language}.json. Even though this is only meant as a +// demo, even this much can be useful in a real life scenario if paired with a backend API that generates the files for +// other languages using PO files, to centralize the localization tooling. +public class BasicJsonLocalizationProcessor : IProjectProcessor +{ + public void Process(string path, string basePath, LocalizableStringCollection strings) + { + ArgumentException.ThrowIfNullOrEmpty(path); + ArgumentException.ThrowIfNullOrEmpty(basePath); + ArgumentNullException.ThrowIfNull(strings); + + var jsonFilePaths = Directory.GetFiles(path, "*.json", SearchOption.AllDirectories) + .Where(path => Path.GetFileNameWithoutExtension(path).ToUpperInvariant() is "EN" or "00" or "IV") + .Where(path => Path.GetFileName(Path.GetDirectoryName(path))?.ToUpperInvariant() is "I18N") + .GroupBy(Path.GetDirectoryName) + .Select(group => group + .OrderBy(path => Path.GetFileNameWithoutExtension(path).ToUpperInvariant() switch + { + "EN" => 0, + "00" => 1, + "IV" => 2, + _ => 3, + }) + .ThenBy(path => path) + .First()); + + foreach (var jsonFilePath in jsonFilePaths) + { + try + { + ProcessJson( + jsonFilePath, + strings, + JObject.Parse(File.ReadAllText(jsonFilePath)), + string.Empty); + } + catch + { + Console.WriteLine("Process failed for: {0}", path); + } + } + } + + private static void ProcessJson(string path, LocalizableStringCollection strings, JsonNode json, string prefix) + { + if (json is JsonObject jsonObject) + { + foreach (var (name, value) in jsonObject) + { + var newPrefix = string.IsNullOrEmpty(prefix) ? name : $"{prefix}.{name}"; + ProcessJson(path, strings, value, newPrefix); + } + + return; + } + + if (json is JsonValue jsonValue) + { + var value = jsonValue.GetObjectValue()?.ToString(); + strings.Add(new() + { + Context = prefix, + Location = new() { SourceFile = path }, + Text = value, + }); + } + } +} + +projectProcessors.Add(new BasicJsonLocalizationProcessor()); \ No newline at end of file diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json new file mode 100644 index 0000000..079af0d --- /dev/null +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json @@ -0,0 +1,13 @@ +{ + "about": { + "title": "About us", + "notes": "Title for main menu" + }, + "home": { + "title": "Home page", + "context": "Displayed on the main website page" + }, + "admin.login": { + "title": "Administrator login" + } +} \ No newline at end of file diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs new file mode 100644 index 0000000..8387c87 --- /dev/null +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs @@ -0,0 +1,66 @@ +using System.Text; +using System.Text.RegularExpressions; +using Xunit; + +namespace OrchardCoreContrib.PoExtractor.Tests; + +public class PluginTests +{ + private const string PluginTestFiles = nameof(PluginTestFiles); + + [Fact] + public async Task WriteRecord_WritesSingularLocalizableString() + { + // Arrange + using var stream = new MemoryStream(); + var plugins = new[] { Path.Join(PluginTestFiles, "BasicJsonLocalizationProcessor.csx") }; + var projectProcessors = new List(); + var projectFiles = new List { Path.Join(PluginTestFiles, "OrchardCoreContrib.PoExtractor.Tests.dll") }; + var localizableStrings = new LocalizableStringCollection(); + + // Act + await Program.ProcessPluginsAsync(plugins, projectProcessors, projectFiles); + projectProcessors[0].Process(PluginTestFiles, Path.GetFileName(Environment.CurrentDirectory), localizableStrings); + + using (var writer = new PoWriter(stream)) + { + writer.WriteRecord(localizableStrings.Values); + } + + // Assert + Assert.Single(projectProcessors); + Assert.Single(projectFiles); + Assert.Equal(5, localizableStrings.Values.Count()); + + const string expectedResult = @" + #: PluginTestFiles/i18n/en.json:0 + msgctxt ""about.title"" + msgid ""About us"" + msgstr """" + + #: PluginTestFiles/i18n/en.json:0 + msgctxt ""about.notes"" + msgid ""Title for main menu"" + msgstr """" + + #: PluginTestFiles/i18n/en.json:0 + msgctxt ""home.title"" + msgid ""Home page"" + msgstr """" + + #: PluginTestFiles/i18n/en.json:0 + msgctxt ""home.context"" + msgid ""Displayed on the main website page"" + msgstr """" + + #: PluginTestFiles/i18n/en.json:0 + msgctxt ""admin.login.title"" + msgid ""Administrator login"" + msgstr """""; + var actualResult = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(CleanupSpaces(expectedResult), CleanupSpaces(actualResult)); + } + + private static string CleanupSpaces(string input) => + Regex.Replace(input.Trim(), @"^\s+", string.Empty, RegexOptions.Multiline); +} From 11b7fd542e52cc1f093e5bccd64cab8f7baaec87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 04:03:54 +0100 Subject: [PATCH 08/32] Fix spacing. --- src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs | 2 +- .../PluginTestFiles/BasicJsonLocalizationProcessor.csx | 2 +- .../PluginTestFiles/i18n/en.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs b/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs index fa1f5a1..2694158 100644 --- a/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs +++ b/src/OrchardCoreContrib.PoExtractor/GetCliOptionsResult.cs @@ -6,4 +6,4 @@ public class GetCliOptionsResult public string TemplateEngine { get; set; } public string SingleOutputFile { get; set; } public IList Plugins { get; set; } = new List(); -} \ No newline at end of file +} diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx index cb4f51e..193447c 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx @@ -74,4 +74,4 @@ public class BasicJsonLocalizationProcessor : IProjectProcessor } } -projectProcessors.Add(new BasicJsonLocalizationProcessor()); \ No newline at end of file +projectProcessors.Add(new BasicJsonLocalizationProcessor()); diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json index 079af0d..97ad51b 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json @@ -10,4 +10,4 @@ "admin.login": { "title": "Administrator login" } -} \ No newline at end of file +} From e7c2a109b7776e8f5e367875365173ddd3fdd8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 11:50:03 +0100 Subject: [PATCH 09/32] Fix test name. --- test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs index 8387c87..6216597 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs @@ -9,7 +9,7 @@ public class PluginTests private const string PluginTestFiles = nameof(PluginTestFiles); [Fact] - public async Task WriteRecord_WritesSingularLocalizableString() + public async Task ProcessPluginsBasicJsonLocalizationProcessor() { // Arrange using var stream = new MemoryStream(); From 8716a947e210d13424b0edecfd10f208b8fa9d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Sat, 7 Dec 2024 11:58:37 +0100 Subject: [PATCH 10/32] Fix test for Windows. --- .../PluginTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs index 6216597..c97bdc8 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs @@ -61,6 +61,12 @@ public async Task ProcessPluginsBasicJsonLocalizationProcessor() Assert.Equal(CleanupSpaces(expectedResult), CleanupSpaces(actualResult)); } - private static string CleanupSpaces(string input) => - Regex.Replace(input.Trim(), @"^\s+", string.Empty, RegexOptions.Multiline); + private static string CleanupSpaces(string input) + { + // Trim leading whitespaces. + input = Regex.Replace(input.Trim(), @"^\s+", string.Empty, RegexOptions.Multiline); + + // Make the path OS-specific, so the test works on Windows as well. + return input.Replace('/', Path.DirectorySeparatorChar); + } } From bd6cfd5b279f6b510da9005cf1a1345639bd1d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 9 Dec 2024 16:19:40 +0100 Subject: [PATCH 11/32] Fix warning NU1507: There are 2 package sources defined in your configuration. --- NuGet.config | 6 +++--- OrchardCoreContrib.PoExtractor.sln | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/NuGet.config b/NuGet.config index 83ba759..aab4418 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,8 +1,8 @@ - + + - - + diff --git a/OrchardCoreContrib.PoExtractor.sln b/OrchardCoreContrib.PoExtractor.sln index 5b1888d..d149bdf 100644 --- a/OrchardCoreContrib.PoExtractor.sln +++ b/OrchardCoreContrib.PoExtractor.sln @@ -36,6 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props README.md = README.md + NuGet.config = NuGet.config EndProjectSection EndProject Global From d6343805fa94fe4ae8cb89e1eb711e43b31a0fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Mon, 9 Dec 2024 16:43:31 +0100 Subject: [PATCH 12/32] Move ProcessPluginsAsync logic into the PluginHelper class in the Abstractions project. --- ...oreContrib.PoExtractor.Abstractions.csproj | 3 ++ .../PluginHelper.cs | 25 +++++++++++++++ .../OrchardCoreContrib.PoExtractor.csproj | 3 -- src/OrchardCoreContrib.PoExtractor/Program.cs | 31 +++++++++---------- 4 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj b/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj index 84ee282..dc81602 100644 --- a/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj +++ b/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj @@ -2,4 +2,7 @@ OrchardCoreContrib.PoExtractor + + + diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs new file mode 100644 index 0000000..84e6381 --- /dev/null +++ b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; + +namespace OrchardCoreContrib.PoExtractor; + +public static class PluginHelper +{ + public static async Task ProcessPluginsAsync( + IList plugins, + List projectProcessors, + List projectFiles, + IEnumerable assemblies) + { + var options = ScriptOptions.Default.AddReferences(assemblies); + + foreach (var plugin in plugins) + { + var code = await File.ReadAllTextAsync(plugin); + await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles)); + } + } + + public record PluginContext(List projectProcessors, List projectFiles); +} \ No newline at end of file diff --git a/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj b/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj index f6b2210..183c158 100644 --- a/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj +++ b/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj @@ -31,7 +31,4 @@ - - - diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index cafa5ca..a14d4f6 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -1,6 +1,4 @@ -using Microsoft.CodeAnalysis.CSharp.Scripting; -using Microsoft.CodeAnalysis.Scripting; -using OrchardCoreContrib.PoExtractor.DotNet; +using OrchardCoreContrib.PoExtractor.DotNet; using OrchardCoreContrib.PoExtractor.DotNet.CS; using OrchardCoreContrib.PoExtractor.DotNet.VB; using OrchardCoreContrib.PoExtractor.Liquid; @@ -131,19 +129,22 @@ public static async Task Main(string[] args) } } - public static async Task ProcessPluginsAsync( + /// + /// A shortcut to that gives the script access to all of the + /// OrchardCoreContrib.PoExtractor.* assemblies. + /// + public static Task ProcessPluginsAsync( IList plugins, List projectProcessors, - List projectFiles) - { - var options = ScriptOptions.Default.AddReferences(typeof(Program).Assembly); - - foreach (var plugin in plugins) - { - var code = await File.ReadAllTextAsync(plugin); - await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles)); - } - } + List projectFiles) => + PluginHelper.ProcessPluginsAsync(plugins, projectProcessors, projectFiles, [ + typeof(IProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.Abstractions + typeof(ExtractingCodeWalker).Assembly, // OrchardCoreContrib.PoExtractor.DotNet + typeof(CSharpProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.DotNet.CS + typeof(VisualBasicProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.DotNet.VB + typeof(LiquidProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.Liquid + typeof(RazorProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.Razor + ]); private static GetCliOptionsResult GetCliOptions(string[] args) { @@ -258,6 +259,4 @@ private static void ShowHelp() Console.WriteLine(" -p, --plugin A path to a C# script (.csx) file which can define further IProjectProcessor"); Console.WriteLine(" implementations. You can have multiple of this switch in a call."); } - - public record PluginContext(List projectProcessors, List projectFiles); } From 132d4f428534d74868e7cfd9697b82e256e115eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 31 Dec 2024 09:57:01 +0100 Subject: [PATCH 13/32] Revert unnecessary nuget source name change. --- NuGet.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.config b/NuGet.config index aab4418..f96ed09 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,6 +3,6 @@ - + From e4abfa704a4968bc1650e1c21deac6c3602fbef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 14 Jan 2025 20:47:55 +0100 Subject: [PATCH 14/32] Fix spacing. --- src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs index 84e6381..608e4c9 100644 --- a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs +++ b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs @@ -22,4 +22,4 @@ public static async Task ProcessPluginsAsync( } public record PluginContext(List projectProcessors, List projectFiles); -} \ No newline at end of file +} From 07798dcdf5b685cd0aea8c3b946b9a130c9e1b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 14 Jan 2025 21:17:17 +0100 Subject: [PATCH 15/32] prevent index out of range exception --- src/OrchardCoreContrib.PoExtractor/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index a14d4f6..d9f1982 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -85,7 +85,8 @@ public static async Task Main(string[] args) var projectPath = Path.GetDirectoryName(projectFile); var projectBasePath = Path.GetDirectoryName(projectPath) + Path.DirectorySeparatorChar; var projectRelativePath = projectPath[projectBasePath.Length..]; - var rootedProject = projectPath[(projectPath.IndexOf(inputPath) + inputPath.Length + 1)..]; + var rootedProject = projectPath == inputPath + ? projectPath : projectPath[(projectPath.IndexOf(inputPath) + inputPath.Length + 1)..]; if (IgnoredProject.ToList().Any(p => rootedProject.StartsWith(p))) { continue; From 9cf82655b513b7a40e13d6877d3c7cd5f73e026a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 14 Jan 2025 22:15:45 +0100 Subject: [PATCH 16/32] Minor code cleanup. --- src/OrchardCoreContrib.PoExtractor/Program.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index d9f1982..dae36ae 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -158,15 +158,16 @@ private static GetCliOptionsResult GetCliOptions(string[] args) for (int i = 4; i <= args.Length; i += 2) { + var item = args[i - 1]; switch (args[i - 2]) { case "-l": case "--language": - if (args[i - 1].Equals(Language.CSharp, StringComparison.CurrentCultureIgnoreCase)) + if (item.Equals(Language.CSharp, StringComparison.CurrentCultureIgnoreCase)) { result.Language = Language.CSharp; } - else if (args[i - 1].Equals(Language.VisualBasic, StringComparison.CurrentCultureIgnoreCase)) + else if (item.Equals(Language.VisualBasic, StringComparison.CurrentCultureIgnoreCase)) { result.Language = Language.VisualBasic; } @@ -178,11 +179,11 @@ private static GetCliOptionsResult GetCliOptions(string[] args) break; case "-t": case "--template": - if (args[i - 1].Equals(TemplateEngine.Razor, StringComparison.CurrentCultureIgnoreCase)) + if (item.Equals(TemplateEngine.Razor, StringComparison.CurrentCultureIgnoreCase)) { result.TemplateEngine = TemplateEngine.Razor; } - else if (args[i - 1].Equals(TemplateEngine.Liquid, StringComparison.CurrentCultureIgnoreCase)) + else if (item.Equals(TemplateEngine.Liquid, StringComparison.CurrentCultureIgnoreCase)) { result.TemplateEngine = TemplateEngine.Liquid; } @@ -194,9 +195,9 @@ private static GetCliOptionsResult GetCliOptions(string[] args) break; case "-i": case "--ignore": - if (!string.IsNullOrEmpty(args[i - 1])) + if (!string.IsNullOrEmpty(item)) { - var ignoredProjects = args[i - 1].Split(',', StringSplitOptions.RemoveEmptyEntries); + var ignoredProjects = item.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (var ignoredProject in ignoredProjects) { @@ -206,9 +207,9 @@ private static GetCliOptionsResult GetCliOptions(string[] args) break; case "--localizer": - if (!string.IsNullOrEmpty(args[i - 1])) + if (!string.IsNullOrEmpty(item)) { - var localizerIdentifiers = args[i - 1].Split(',', StringSplitOptions.RemoveEmptyEntries); + var localizerIdentifiers = item.Split(',', StringSplitOptions.RemoveEmptyEntries); LocalizerAccessors.LocalizerIdentifiers = localizerIdentifiers; } @@ -216,9 +217,9 @@ private static GetCliOptionsResult GetCliOptions(string[] args) break; case "-s": case "--single": - if (!string.IsNullOrEmpty(args[i - 1])) + if (!string.IsNullOrEmpty(item)) { - result.SingleOutputFile = args[i - 1]; + result.SingleOutputFile = item; } break; @@ -226,7 +227,7 @@ private static GetCliOptionsResult GetCliOptions(string[] args) case "--plugin": if (File.Exists(args[i - 1])) { - result.Plugins.Add(args[i - 1]); + result.Plugins.Add(item); } break; From 857c41f688296e8697ab1a3416ec9b3bd017c21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 14 Jan 2025 22:18:33 +0100 Subject: [PATCH 17/32] Add online plugin support as suggested by @sebastienros. --- README.md | 4 ++-- .../PluginHelper.cs | 4 +++- src/OrchardCoreContrib.PoExtractor/Program.cs | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d6ec4bf..43efa52 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ Specifies the code language to extracts translatable strings from. Default: `C#` Specifies the template engine to extract the translatable strings from. Default: `Razor` & `Liquid` templates. -- **`-p|--plugin {path to CSX file}`** +- **`-p|--plugin {path or URL to CSX file}`** -Specifies a path to a C# script file which can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of this switch in one call to load several plugins at once. +Specifies a path to a C# script file which can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of this switch in one call to load several plugins at once. If the argument starts with `https://` then it's treated as a web URL and the script at that address is downloaded into memory and executed instead of a local file. When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are automatically loaded, and two globals are defined: diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs index 608e4c9..db07885 100644 --- a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs +++ b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs @@ -16,7 +16,9 @@ public static async Task ProcessPluginsAsync( foreach (var plugin in plugins) { - var code = await File.ReadAllTextAsync(plugin); + var code = plugin.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + ? await new HttpClient().GetStringAsync(plugin) + : await File.ReadAllTextAsync(plugin); await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles)); } } diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index dae36ae..fa4c92f 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -1,4 +1,5 @@ -using OrchardCoreContrib.PoExtractor.DotNet; +using OrchardCore.Modules; +using OrchardCoreContrib.PoExtractor.DotNet; using OrchardCoreContrib.PoExtractor.DotNet.CS; using OrchardCoreContrib.PoExtractor.DotNet.VB; using OrchardCoreContrib.PoExtractor.Liquid; @@ -225,7 +226,7 @@ private static GetCliOptionsResult GetCliOptions(string[] args) break; case "-p": case "--plugin": - if (File.Exists(args[i - 1])) + if (File.Exists(item) || item.StartsWithOrdinalIgnoreCase("https://")) { result.Plugins.Add(item); } @@ -258,7 +259,7 @@ private static void ShowHelp() Console.WriteLine(" -i, --ignore project1,project2 Ignores extracting PO filed from a given project(s)."); Console.WriteLine(" --localizer localizer1,localizer2 Specifies the name of the localizer(s) that will be used during the extraction process."); Console.WriteLine(" -s, --single Specifies the single output file."); - Console.WriteLine(" -p, --plugin A path to a C# script (.csx) file which can define further IProjectProcessor"); - Console.WriteLine(" implementations. You can have multiple of this switch in a call."); + Console.WriteLine(" -p, --plugin A path or web URL with HTTPS scheme to a C# script (.csx) file which can define further"); + Console.WriteLine(" IProjectProcessor implementations. You can have multiple of this switch in a call."); } } From f6e04436f635cb7b4d2d4d384d702d2b0c67d387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 15 Jan 2025 14:00:27 +0100 Subject: [PATCH 18/32] Try out online referencing. --- .../PluginTestFiles/BasicJsonLocalizationProcessor.csx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx index 193447c..ad1cecb 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx @@ -1,8 +1,12 @@ +#r "../../../../OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" + using System; using System.IO; using System.Linq; using System.Text.Json.Nodes; using OrchardCoreContrib.PoExtractor; +using OrchardCore.Commerce.Constants; +Console.WriteLine("Imported resource: {0}", ResourceNames.ShoppingCart); // This example plugin implements processing for a very simplistic subset of the i18next JSON format. It only supports // strings and other objects, and the files must be located in i18n/{language}.json. Even though this is only meant as a From 1517c35c4c541685508fd609bb3b57ec3c7603fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 15 Jan 2025 14:01:35 +0100 Subject: [PATCH 19/32] Try out online referencing. --- .../PluginTestFiles/BasicJsonLocalizationProcessor.csx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx index ad1cecb..b59960f 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx @@ -1,4 +1,4 @@ -#r "../../../../OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" +#r "../../../OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" using System; using System.IO; From 4654924e6691962d166e6fd36231a438fc05be61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 15 Jan 2025 14:03:44 +0100 Subject: [PATCH 20/32] Add support for importing DLLs with a relative path. --- README.md | 8 +++++++ .../PluginHelper.cs | 21 +++++++++++++++---- .../BasicJsonLocalizationProcessor.csx | 4 ---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 43efa52..5e6c3b7 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,14 @@ When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are - `List projectProcessors`: Add an instance of your custom `IProjectProcessor` implementation type to this list. - `List projectFiles`: In the unlikely case that you have to add a new project file type (such as _.fsproj_) add the project file paths to this list. +> [!TIP] +> You can't import NuGet packages in your script file, but you can import local DLL files using the `#r "path/to/package.dll"` directive. The path can be relative to the script file's location. (For remote scripts loaded with a URL, the path can be relative to the current working directory.) For example: +> ```csharp +> #r "src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" +> using OrchardCore.Commerce.Constants; +> Console.WriteLine("Imported resource name: {0}", ResourceNames.ShoppingCart); +> ``` + ## Uninstallation ```powershell diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs index db07885..4f99cfb 100644 --- a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs +++ b/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs @@ -12,13 +12,26 @@ public static async Task ProcessPluginsAsync( List projectFiles, IEnumerable assemblies) { - var options = ScriptOptions.Default.AddReferences(assemblies); + var sharedOptions = ScriptOptions.Default.AddReferences(assemblies); foreach (var plugin in plugins) { - var code = plugin.StartsWith("https://", StringComparison.OrdinalIgnoreCase) - ? await new HttpClient().GetStringAsync(plugin) - : await File.ReadAllTextAsync(plugin); + string code; + ScriptOptions options; + + if (plugin.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + code = await new HttpClient().GetStringAsync(plugin); + options = sharedOptions.WithFilePath(Path.Join( + Environment.CurrentDirectory, + Path.GetFileName(new Uri(plugin).AbsolutePath))); + } + else + { + code = await File.ReadAllTextAsync(plugin); + options = sharedOptions.WithFilePath(Path.GetFullPath(plugin)); + } + await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles)); } } diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx index b59960f..193447c 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx @@ -1,12 +1,8 @@ -#r "../../../OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" - using System; using System.IO; using System.Linq; using System.Text.Json.Nodes; using OrchardCoreContrib.PoExtractor; -using OrchardCore.Commerce.Constants; -Console.WriteLine("Imported resource: {0}", ResourceNames.ShoppingCart); // This example plugin implements processing for a very simplistic subset of the i18next JSON format. It only supports // strings and other objects, and the files must be located in i18n/{language}.json. Even though this is only meant as a From 3a30702084cac933bae12208301c708c7b82c50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 15 Jan 2025 14:05:19 +0100 Subject: [PATCH 21/32] Move PluginHelper to the main project instead of Abstractions --- .../OrchardCoreContrib.PoExtractor.Abstractions.csproj | 3 --- .../OrchardCoreContrib.PoExtractor.csproj | 3 +++ .../PluginHelper.cs | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{OrchardCoreContrib.PoExtractor.Abstractions => OrchardCoreContrib.PoExtractor}/PluginHelper.cs (100%) diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj b/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj index dc81602..84ee282 100644 --- a/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj +++ b/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj @@ -2,7 +2,4 @@ OrchardCoreContrib.PoExtractor - - - diff --git a/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj b/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj index 183c158..f6b2210 100644 --- a/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj +++ b/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj @@ -31,4 +31,7 @@ + + + diff --git a/src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs b/src/OrchardCoreContrib.PoExtractor/PluginHelper.cs similarity index 100% rename from src/OrchardCoreContrib.PoExtractor.Abstractions/PluginHelper.cs rename to src/OrchardCoreContrib.PoExtractor/PluginHelper.cs From bb7aa1628f590012869ef66d4574e24d57f7771b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Wed, 15 Jan 2025 14:21:03 +0100 Subject: [PATCH 22/32] Improve tip wording. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e6c3b7..054a21e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are - `List projectFiles`: In the unlikely case that you have to add a new project file type (such as _.fsproj_) add the project file paths to this list. > [!TIP] -> You can't import NuGet packages in your script file, but you can import local DLL files using the `#r "path/to/package.dll"` directive. The path can be relative to the script file's location. (For remote scripts loaded with a URL, the path can be relative to the current working directory.) For example: +> You can't import NuGet packages in your script file, but you can import local DLL files using the `#r "path/to/package.dll"` directive. The path can be relative to the script file's location so you can import packages from the build directory of the project you are extracting from. This can be especially useful if you launch the tool as using MSBuild as a post-build action. (For remote scripts loaded with a URL, the path can be relative to the current working directory.) For example: +> > ```csharp > #r "src/Modules/OrchardCore.Commerce/bin/Debug/net8.0/OrchardCore.Commerce.dll" > using OrchardCore.Commerce.Constants; From af5a94a54c99860dabaee313a9e37762be84fdc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 16 Jan 2025 14:50:50 +0100 Subject: [PATCH 23/32] Update README.md Co-authored-by: Hisham Bin Ateya --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 054a21e..21ca01c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Specifies the template engine to extract the translatable strings from. Default: - **`-p|--plugin {path or URL to CSX file}`** -Specifies a path to a C# script file which can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of this switch in one call to load several plugins at once. If the argument starts with `https://` then it's treated as a web URL and the script at that address is downloaded into memory and executed instead of a local file. +Specifies a path to a C# script file that can define further project processors. (You can find an example script [here](test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx).) This can be used to process localization from code languages or template engines not supported by the above options. You can have multiple of these switches in one call to load several plugins simultaneously. If the argument starts with `https://` then it's treated as a web URL and the script at that address is downloaded into memory and executed instead of a local file. When executing the plugins, all _OrchardCoreContrib.PoExtractor_ assemblies are automatically loaded, and two globals are defined: From 20ecc7d264ca97805948e801c970a8418db034d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 16 Jan 2025 15:52:29 +0100 Subject: [PATCH 24/32] Post-merge rewrite. --- .../PluginHelper.cs | 19 +++++++++++-- src/OrchardCoreContrib.PoExtractor/Program.cs | 27 ++++++++++++++++--- .../PluginTests.cs | 2 +- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/OrchardCoreContrib.PoExtractor/PluginHelper.cs b/src/OrchardCoreContrib.PoExtractor/PluginHelper.cs index 4f99cfb..64adf58 100644 --- a/src/OrchardCoreContrib.PoExtractor/PluginHelper.cs +++ b/src/OrchardCoreContrib.PoExtractor/PluginHelper.cs @@ -1,17 +1,32 @@ using System.Reflection; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; +using OrchardCoreContrib.PoExtractor.DotNet; +using OrchardCoreContrib.PoExtractor.DotNet.CS; +using OrchardCoreContrib.PoExtractor.DotNet.VB; +using OrchardCoreContrib.PoExtractor.Liquid; +using OrchardCoreContrib.PoExtractor.Razor; namespace OrchardCoreContrib.PoExtractor; public static class PluginHelper { public static async Task ProcessPluginsAsync( - IList plugins, + IEnumerable plugins, List projectProcessors, List projectFiles, - IEnumerable assemblies) + IEnumerable assemblies = null) { + assemblies ??= + [ + typeof(IProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.Abstractions + typeof(ExtractingCodeWalker).Assembly, // OrchardCoreContrib.PoExtractor.DotNet + typeof(CSharpProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.DotNet.CS + typeof(VisualBasicProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.DotNet.VB + typeof(LiquidProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.Liquid + typeof(RazorProjectProcessor).Assembly, // OrchardCoreContrib.PoExtractor.Razor + ]; + var sharedOptions = ScriptOptions.Default.AddReferences(assemblies); foreach (var plugin in plugins) diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index cbb87fa..d5e3b18 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -1,4 +1,6 @@ -using McMaster.Extensions.CommandLineUtils; +using System.ComponentModel.DataAnnotations; +using McMaster.Extensions.CommandLineUtils; +using OrchardCore.Modules; using OrchardCoreContrib.PoExtractor.DotNet; using OrchardCoreContrib.PoExtractor.DotNet.CS; using OrchardCoreContrib.PoExtractor.DotNet.VB; @@ -33,8 +35,18 @@ public static void Main(string[] args) var ignoredProjects = app.Option("-i|--ignore ", "Ignores extracting PO files from a given project(s).", CommandOptionType.MultipleValue); var localizers = app.Option("--localizer ", "Specifies the name of the localizer(s) that will be used during the extraction process.", CommandOptionType.MultipleValue); var single = app.Option("-s|--single ", "Specifies the single output file.", CommandOptionType.SingleValue); - - app.OnExecute(() => + var plugins = app.Option( + "-p|--plugin ", + "A path or web URL with HTTPS scheme to a C# script (.csx) file which can define further " + + "IProjectProcessor implementations. You can have multiple of this switch in a call.", + CommandOptionType.MultipleValue, + option => option.OnValidate(_ => option + .Values + .All(item => File.Exists(item) || item.StartsWithOrdinalIgnoreCase("https://")) + ? ValidationResult.Success + : new ValidationResult("Plugin must be an existing local file or a valid HTTPS URL."))); + + app.OnExecuteAsync(async cancellationToken => { if (!Directory.Exists(inputPath.Value)) { @@ -84,6 +96,11 @@ public static void Main(string[] args) projectProcessors.Add(new LiquidProjectProcessor()); } + if (plugins.Values.Count > 0) + { + await PluginHelper.ProcessPluginsAsync(plugins.Values, projectProcessors, projectFiles); + } + var isSingleFileOutput = !string.IsNullOrEmpty(single.Value()); var localizableStrings = new LocalizableStringCollection(); foreach (var projectFile in projectFiles) @@ -91,7 +108,9 @@ public static void Main(string[] args) var projectPath = Path.GetDirectoryName(projectFile); var projectBasePath = Path.GetDirectoryName(projectPath) + Path.DirectorySeparatorChar; var projectRelativePath = projectPath[projectBasePath.Length..]; - var rootedProject = projectPath[(projectPath.IndexOf(inputPath.Value) + inputPath.Value.Length + 1)..]; + var rootedProject = projectPath == inputPath.Value + ? projectPath + : projectPath[(projectPath.IndexOf(inputPath.Value, StringComparison.Ordinal) + inputPath.Value.Length + 1)..]; if (IgnoredProject.ToList().Any(p => rootedProject.StartsWith(p))) { continue; diff --git a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs index c97bdc8..a44408d 100644 --- a/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs +++ b/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs @@ -19,7 +19,7 @@ public async Task ProcessPluginsBasicJsonLocalizationProcessor() var localizableStrings = new LocalizableStringCollection(); // Act - await Program.ProcessPluginsAsync(plugins, projectProcessors, projectFiles); + await PluginHelper.ProcessPluginsAsync(plugins, projectProcessors, projectFiles); projectProcessors[0].Process(PluginTestFiles, Path.GetFileName(Environment.CurrentDirectory), localizableStrings); using (var writer = new PoWriter(stream)) From 1c03367dc11ead027c94c34bafdba7a43f6e8b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Thu, 16 Jan 2025 15:54:14 +0100 Subject: [PATCH 25/32] fix pluralization. --- src/OrchardCoreContrib.PoExtractor/Program.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OrchardCoreContrib.PoExtractor/Program.cs b/src/OrchardCoreContrib.PoExtractor/Program.cs index d5e3b18..7f21bc4 100644 --- a/src/OrchardCoreContrib.PoExtractor/Program.cs +++ b/src/OrchardCoreContrib.PoExtractor/Program.cs @@ -24,13 +24,13 @@ public static void Main(string[] args) .IsRequired(); // Options - var language = app.Option("-l|--language ", "Specifies the code language to extracts translatable strings from.", CommandOptionType.SingleValue, options => + var language = app.Option("-l|--language ", "Specifies the code language to extracts translatable strings from.", CommandOptionType.SingleValue, option => { - options.Accepts(cfg => cfg.Values("C#", "VB")); - options.DefaultValue = "C#"; + option.Accepts(cfg => cfg.Values("C#", "VB")); + option.DefaultValue = "C#"; }); - var template = app.Option("-t|--template