();
99 |
100 | if (settings.Required && String.IsNullOrWhiteSpace(field.Url))
101 | {
102 | updater.ModelState.AddModelError(Prefix, S["The url is required for {0}.", context.PartFieldDefinition.DisplayName()]);
103 | }
104 | }
105 |
106 | return Edit(field, context);
107 | }
108 | }
109 | }
110 | ```
111 |
112 | VB code:
113 | ```vb
114 | Namespace OrchardCore.Modules.GreetingModule
115 | Public Class Greeting
116 | private readonly S As IStringLocalizer(Of Greeting)
117 |
118 | Public Sub New(ByVal localizer As IStringLocalizer(Of Greeting))
119 | S = localizer
120 | End Sub
121 |
122 | Public Sub Saulation(byVal name As String)
123 | Console.WriteLine(S("Hi {0} ...", name))
124 | End Sub
125 | End Class
126 | End Namespace
127 | ```
128 |
129 | Razor view:
130 | ```html
131 | @model OrchardCore.ContentFields.ViewModels.EditLinkFieldViewModel
132 |
133 |
134 |
137 |
140 |
144 |
145 |
146 | ```
147 |
148 | Liquid template:
149 | ```html
150 | div class="page-heading">
151 | {{ "Page Not Found" | t }}
152 | /div>
153 |
154 | ```
155 |
156 | Generated POT file:
157 | ```
158 | #: OrchardCore.ContentFields\Drivers\LinkFieldDriver.cs:59
159 | #. updater.ModelState.AddModelError(Prefix, T["The url is required for {0}.", context.PartFieldDefinition.DisplayName()]);
160 | msgctxt "OrchardCore.ContentFields.Fields.LinkFieldDisplayDriver"
161 | msgid "The url is required for {0}."
162 | msgstr ""
163 |
164 | #: OrchardCore.Modules.GreetingModule\Greeting.vb:94
165 | #. Console.WriteLine(S("Hi {0} ...", name))
166 | msgctxt "OrchardCore.Modules.GreetingModule.Greeting"
167 | msgid "Hi {0} ..."
168 | msgstr ""
169 |
170 | #: OrchardCore.ContentFields\Views\LinkField.Edit.cshtml:32
171 | #.
172 | msgctxt "OrchardCore.ContentFields.Views.LinkField.Edit"
173 | msgid "Link text"
174 | msgstr ""
175 |
176 | #: TheBlogTheme\Views\Shared\NotFound.liquid:0
177 | msgctxt "TheBlogTheme.Views.Shared.NotFound"
178 | msgid "Page Not Found"
179 | msgstr ""
180 | ```
181 |
182 | ## Credits
183 |
184 | **PoExtractor**
185 |
186 | https://github.com/lukaskabrt/PoExtractor
187 |
188 | Lukas Kabrt
189 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.0",
4 | "rollForward": "latestMinor"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/images/OCC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor/f799acb7c556054bb2f1af0c3be8ead67eb3392c/images/OCC.png
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor/f799acb7c556054bb2f1af0c3be8ead67eb3392c/images/icon.png
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 Lukas Kabrt
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace OrchardCoreContrib.PoExtractor;
4 |
5 | ///
6 | /// Extension methods for .
7 | ///
8 | public static class StringExtensions
9 | {
10 | ///
11 | /// Removes the given value from the start of the text.
12 | ///
13 | /// The source text.
14 | /// The value to be trimmed.
15 | public static string TrimStart(this string text, string trimText)
16 | {
17 | if (string.IsNullOrEmpty(text))
18 | {
19 | throw new ArgumentException($"'{nameof(text)}' cannot be null or empty.", nameof(text));
20 | }
21 |
22 | if (string.IsNullOrEmpty(trimText))
23 | {
24 | throw new ArgumentException($"'{nameof(trimText)}' cannot be null or empty.", nameof(trimText));
25 | }
26 |
27 | var index = text.IndexOf(trimText);
28 |
29 | return index < 0
30 | ? text
31 | : text.Remove(index, trimText.Length);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/IMetadataProvider.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | ///
4 | /// Provides metadata of the translatable text based on information from the AST node.
5 | ///
6 | /// Type of the node.
7 | public interface IMetadataProvider
8 | {
9 | ///
10 | /// Gets context of the translatable text.
11 | ///
12 | /// The AST node representing the translatable text.
13 | /// A string value, that is used in the output file as #msgctx.
14 | string GetContext(TNode node);
15 |
16 | ///
17 | /// Gets location of the translatable text in the source file.
18 | ///
19 | /// The AST node representing the translatable text.
20 | /// An object with the description of the location in the source file.
21 | LocalizableStringLocation GetLocation(TNode node);
22 | }
23 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/IProjectProcessor.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | ///
4 | /// Contract for processing a project to get the localization strings.
5 | ///
6 | public interface IProjectProcessor
7 | {
8 | ///
9 | /// Lookup for the localizable string by process the given project path.
10 | ///
11 | /// Project path.
12 | /// Project base path.
13 | /// List of contain in the processed project.
14 | void Process(string path, string basePath, LocalizableStringCollection localizableStrings);
15 | }
16 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/IStringExtractor.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | ///
4 | /// Extracts a translatable string from a node of the AST tree.
5 | ///
6 | /// Type of the node
7 | public interface IStringExtractor
8 | {
9 | ///
10 | /// Tries to extract a localizable string from the AST node.
11 | ///
12 | /// The AST node.
13 | /// The extracted localizable string.
14 | /// true if a localizable string was successfully extracted, otherwise returns false.
15 | bool TryExtract(TNode node, out LocalizableStringOccurence result);
16 | }
17 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableString.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace OrchardCoreContrib.PoExtractor;
5 |
6 | ///
7 | /// Represents a localizable text with all it's occurrences in the project.
8 | ///
9 | public class LocalizableString
10 | {
11 | ///
12 | /// Creates a new instance of the .
13 | ///
14 | public LocalizableString()
15 | {
16 | Locations = [];
17 | }
18 |
19 | ///
20 | /// Creates a new instance of the and properties with data from the source.
21 | ///
22 | /// the with the data.
23 | public LocalizableString(LocalizableStringOccurence source)
24 | {
25 | ArgumentNullException.ThrowIfNull(source);
26 |
27 | Text = source.Text;
28 | TextPlural = source.TextPlural;
29 | Context = source.Context;
30 |
31 | Locations = [ source.Location ];
32 | }
33 |
34 | ///
35 | /// Gets or sets context of the.
36 | ///
37 | public string Context { get; set; }
38 |
39 | ///
40 | /// Gets or sets the localizable text.
41 | ///
42 | public string Text { get; set; }
43 |
44 | ///
45 | /// Gets or sets the localizable text for the plural.
46 | ///
47 | public string TextPlural { get; set; }
48 |
49 | ///
50 | /// Gets collection of all locations of the text in the project.
51 | ///
52 | public List Locations { get; }
53 | }
54 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace OrchardCoreContrib.PoExtractor;
5 |
6 | ///
7 | /// Represents collection of the all localizable strings in the project. Localizable strings with the same values are merged.
8 | ///
9 | public class LocalizableStringCollection
10 | {
11 | private readonly Dictionary _values;
12 |
13 | ///
14 | /// Creates a new empty instance of the class.
15 | ///
16 | public LocalizableStringCollection()
17 | {
18 | _values = [];
19 | }
20 |
21 | ///
22 | /// Gets collection of all in the project.
23 | ///
24 | public IEnumerable Values => _values.Values;
25 |
26 | ///
27 | /// Adds to the collection.
28 | ///
29 | /// The item to add.
30 | public void Add(LocalizableStringOccurence item)
31 | {
32 | ArgumentNullException.ThrowIfNull(item);
33 |
34 | var key = item.Context + item.Text;
35 | if (_values.TryGetValue(key, out var localizedString))
36 | {
37 | localizedString.Locations.Add(item.Location);
38 | }
39 | else
40 | {
41 | _values.Add(key, new LocalizableString(item));
42 | }
43 | }
44 |
45 | ///
46 | /// Clear collection
47 | ///
48 | public void Clear() => _values.Clear();
49 | }
50 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace OrchardCoreContrib.PoExtractor;
4 |
5 | ///
6 | /// Represents a base class for extracting a localizable strings.
7 | ///
8 | /// The type of the node.
9 | ///
10 | /// Creates a new instance of a .
11 | ///
12 | /// The .
13 | public abstract class LocalizableStringExtractor(IMetadataProvider metadataProvider) : IStringExtractor
14 | {
15 | protected IMetadataProvider MetadataProvider { get; } = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider));
16 |
17 | ///
18 | public abstract bool TryExtract(TNode node, out LocalizableStringOccurence result);
19 |
20 | ///
21 | /// Creates a localized string.
22 | ///
23 | /// The localized text.
24 | /// The pluralization form for the localized text.
25 | /// The node in which to get the localized string information.
26 | protected LocalizableStringOccurence CreateLocalizedString(string text, string textPlural, TNode node)
27 | {
28 | if (string.IsNullOrEmpty(text))
29 | {
30 | throw new ArgumentException($"'{nameof(text)}' cannot be null or empty.", nameof(text));
31 | }
32 |
33 | var result = new LocalizableStringOccurence
34 | {
35 | Text = text,
36 | TextPlural = textPlural,
37 | Location = metadataProvider.GetLocation(node),
38 | Context = metadataProvider.GetContext(node)
39 | };
40 |
41 | return result;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringLocation.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | ///
4 | /// Represents a location of the localizable string occurrence in the source code.
5 | ///
6 | public class LocalizableStringLocation
7 | {
8 | ///
9 | /// Gets or sets the name of the source file.
10 | ///
11 | public string SourceFile { get; set; }
12 |
13 | ///
14 | /// Gets or sets the line number in the source file.
15 | ///
16 | public int SourceFileLine { get; set; }
17 |
18 | ///
19 | /// Gets or sets a comment for the occurrence.
20 | ///
21 | ///
22 | /// Typically used to provide better understanding for translators, e.g. copy of the whole line from the source code.
23 | ///
24 | public string Comment { get; set; }
25 | }
26 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/LocalizableStringOccurence.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | ///
4 | /// Represents the specific occurrence of the localizable string in the project.
5 | ///
6 | public class LocalizableStringOccurence
7 | {
8 | ///
9 | /// Gets or sets the context for the localizable string.
10 | ///
11 | public string Context { get; set; }
12 |
13 | ///
14 | /// Gets or sets the localizable text.
15 | ///
16 | public string Text { get; set; }
17 |
18 | ///
19 | /// Gets or sets the localizable pluralization text.
20 | ///
21 | public string TextPlural { get; set; }
22 |
23 | ///
24 | /// Gets or sets the location for the localizable string.
25 | ///
26 | public LocalizableStringLocation Location { get; set; }
27 | }
28 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Abstractions/OrchardCoreContrib.PoExtractor.Abstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | OrchardCoreContrib.PoExtractor
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Core/OrchardCoreContrib.PoExtractor.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | OrchardCoreContrib.PoExtractor
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.CS/CSharpProjectProcessor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders;
3 | using System;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS;
8 |
9 | ///
10 | /// Extracts localizable strings from all *.cs files in the project path.
11 | ///
12 | public class CSharpProjectProcessor : IProjectProcessor
13 | {
14 | private static readonly string _cSharpExtension = "*.cs";
15 |
16 | ///
17 | public virtual void Process(string path, string basePath, LocalizableStringCollection localizableStrings)
18 | {
19 | if (string.IsNullOrEmpty(path))
20 | {
21 | throw new ArgumentException($"'{nameof(path)}' cannot be null or empty.", nameof(path));
22 | }
23 |
24 | if (string.IsNullOrEmpty(basePath))
25 | {
26 | throw new ArgumentException($"'{nameof(basePath)}' cannot be null or empty.", nameof(basePath));
27 | }
28 |
29 | ArgumentNullException.ThrowIfNull(localizableStrings);
30 |
31 | var csharpMetadataProvider = new CSharpMetadataProvider(basePath);
32 | var csharpWalker = new ExtractingCodeWalker(
33 | [
34 | new SingularStringExtractor(csharpMetadataProvider),
35 | new PluralStringExtractor(csharpMetadataProvider),
36 | new ErrorMessageAnnotationStringExtractor(csharpMetadataProvider),
37 | new DisplayAttributeDescriptionStringExtractor(csharpMetadataProvider),
38 | new DisplayAttributeNameStringExtractor(csharpMetadataProvider),
39 | new DisplayAttributeGroupNameStringExtractor(csharpMetadataProvider),
40 | new DisplayAttributeShortNameStringExtractor(csharpMetadataProvider)
41 | ], localizableStrings);
42 |
43 | foreach (var file in Directory.EnumerateFiles(path, $"*{_cSharpExtension}", SearchOption.AllDirectories).OrderBy(file => file))
44 | {
45 | if (file.StartsWith(Path.Combine(path, "obj")))
46 | {
47 | continue;
48 | }
49 |
50 | using var stream = File.OpenRead(file);
51 | using var reader = new StreamReader(stream);
52 | var syntaxTree = CSharpSyntaxTree.ParseText(reader.ReadToEnd(), path: file);
53 |
54 | csharpWalker.Visit(syntaxTree.GetRoot());
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.CS/MetadataProviders/CSharpMetadataProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 | using System;
4 | using System.Linq;
5 |
6 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders;
7 |
8 | ///
9 | /// Provides metadata for C# code files.
10 | ///
11 | public class CSharpMetadataProvider : IMetadataProvider
12 | {
13 | private readonly string _basePath;
14 |
15 | ///
16 | /// Creates a new instance of a .
17 | ///
18 | /// The base path.
19 | public CSharpMetadataProvider(string basePath)
20 | {
21 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath));
22 |
23 | _basePath = basePath;
24 | }
25 |
26 | ///
27 | public string GetContext(SyntaxNode node)
28 | {
29 | ArgumentNullException.ThrowIfNull(node);
30 |
31 | var @namespace = node.Ancestors()
32 | .OfType()
33 | .FirstOrDefault()?
34 | .Name.ToString();
35 |
36 | if (string.IsNullOrEmpty(@namespace))
37 | {
38 | @namespace = node.Ancestors()
39 | .OfType()
40 | .FirstOrDefault()?
41 | .Name.ToString();
42 | }
43 |
44 | var classes = node
45 | .Ancestors()
46 | .OfType()
47 | .Select(c => c.Identifier.ValueText);
48 |
49 | var @class = classes.Count() == 1
50 | ? classes.Single()
51 | : String.Join('.', classes.Reverse());
52 |
53 | return string.IsNullOrEmpty(@namespace)
54 | ? @class
55 | : $"{@namespace}.{@class}";
56 | }
57 |
58 | ///
59 | public LocalizableStringLocation GetLocation(SyntaxNode node)
60 | {
61 | ArgumentNullException.ThrowIfNull(node);
62 |
63 | var lineNumber = node
64 | .GetLocation()
65 | .GetMappedLineSpan()
66 | .StartLinePosition.Line;
67 |
68 | return new LocalizableStringLocation
69 | {
70 | SourceFileLine = lineNumber + 1,
71 | SourceFile = node.SyntaxTree.FilePath.TrimStart(_basePath),
72 | Comment = node.SyntaxTree.GetText().Lines[lineNumber].ToString().Trim()
73 | };
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.CS/OrchardCoreContrib.PoExtractor.DotNet.CS.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.CS/PluralStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using System;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS;
8 |
9 | ///
10 | /// Extracts with the singular text from the C# AST node
11 | ///
12 | ///
13 | /// The localizable string is identified by the name convention - T.Plural(count, "1 book", "{0} books")
14 | ///
15 | ///
16 | /// Creates a new instance of a .
17 | ///
18 | /// The .
19 | public class PluralStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider)
20 | {
21 |
22 | ///
23 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result)
24 | {
25 | ArgumentNullException.ThrowIfNull(nameof(node));
26 |
27 | result = null;
28 |
29 | if (node is InvocationExpressionSyntax invocation &&
30 | invocation.Expression is MemberAccessExpressionSyntax accessor &&
31 | accessor.Expression is IdentifierNameSyntax identifierName &&
32 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) &&
33 | accessor.Name.Identifier.Text == "Plural")
34 | {
35 |
36 | var arguments = invocation.ArgumentList.Arguments;
37 | if (arguments.Count >= 2 &&
38 | arguments[1].Expression is ArrayCreationExpressionSyntax array)
39 | {
40 | if (array.Type.ElementType is PredefinedTypeSyntax arrayType &&
41 | arrayType.Keyword.Text == "string" &&
42 | array.Initializer.Expressions.Count >= 2 &&
43 | array.Initializer.Expressions[0] is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) &&
44 | array.Initializer.Expressions[1] is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression))
45 | {
46 |
47 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node);
48 |
49 | return true;
50 | }
51 | }
52 | else
53 | {
54 | if (arguments.Count >= 3 &&
55 | arguments[1].Expression is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) &&
56 | arguments[2].Expression is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression))
57 | {
58 |
59 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node);
60 |
61 | return true;
62 | }
63 | }
64 | }
65 |
66 | return false;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.CS/SingularStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using System;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS;
8 |
9 | ///
10 | /// Extracts with the singular text from the C# AST node
11 | ///
12 | ///
13 | /// The localizable string is identified by the name convention - T["TEXT TO TRANSLATE"]
14 | ///
15 | ///
16 | /// Creates a new instance of a .
17 | ///
18 | /// The .
19 | public class SingularStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider)
20 | {
21 |
22 | ///
23 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result)
24 | {
25 | ArgumentNullException.ThrowIfNull(node);
26 |
27 | result = null;
28 |
29 | if (node is ElementAccessExpressionSyntax accessor &&
30 | accessor.Expression is IdentifierNameSyntax identifierName &&
31 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) &&
32 | accessor.ArgumentList != null)
33 | {
34 |
35 | var argument = accessor.ArgumentList.Arguments.FirstOrDefault();
36 | if (argument != null && argument.Expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
37 | {
38 | result = CreateLocalizedString(literal.Token.ValueText, null, node);
39 | return true;
40 | }
41 | }
42 |
43 | return false;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.VB/MetadataProviders/VisualBasicMetadataProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.VisualBasic.Syntax;
3 | using System;
4 | using System.Linq;
5 |
6 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders;
7 |
8 | ///
9 | /// Provides metadata for .vb code files
10 | ///
11 | public class VisualBasicMetadataProvider : IMetadataProvider
12 | {
13 | private readonly string _basePath;
14 |
15 | ///
16 | /// Creates a new instance of a .
17 | ///
18 | /// The base path.
19 | public VisualBasicMetadataProvider(string basePath)
20 | {
21 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath));
22 |
23 | _basePath = basePath;
24 | }
25 |
26 | ///
27 | public string GetContext(SyntaxNode node)
28 | {
29 | ArgumentNullException.ThrowIfNull(node);
30 |
31 | var @namespace = node
32 | .Ancestors()
33 | .OfType()
34 | .FirstOrDefault()?.NamespaceStatement.Name
35 | .ToString();
36 |
37 | var classes = node
38 | .Ancestors()
39 | .OfType()
40 | .Select(c => c.ClassStatement.Identifier.ValueText);
41 |
42 | var @class = classes.Count() == 1
43 | ? classes.Single()
44 | : String.Join('.', classes.Reverse());
45 |
46 | return $"{@namespace}.{@class}";
47 | }
48 |
49 | ///
50 | public LocalizableStringLocation GetLocation(SyntaxNode node)
51 | {
52 | ArgumentNullException.ThrowIfNull(node);
53 |
54 | var lineNumber = node.GetLocation().GetMappedLineSpan().StartLinePosition.Line;
55 |
56 | return new LocalizableStringLocation
57 | {
58 | SourceFileLine = lineNumber + 1,
59 | SourceFile = node.SyntaxTree.FilePath.TrimStart(_basePath),
60 | Comment = node.SyntaxTree.GetText().Lines[lineNumber].ToString().Trim()
61 | };
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.VB/OrchardCoreContrib.PoExtractor.DotNet.VB.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.VB/PluralStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.VisualBasic;
3 | using Microsoft.CodeAnalysis.VisualBasic.Syntax;
4 | using System;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB;
8 |
9 | ///
10 | /// Extracts with the singular text from the VB AST node.
11 | ///
12 | ///
13 | /// The localizable string is identified by the name convention - T.Plural(count, "1 book", "{0} books").
14 | ///
15 | ///
16 | /// Creates a new instance of a .
17 | ///
18 | /// The .
19 | public class PluralStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider)
20 | {
21 |
22 | ///
23 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result)
24 | {
25 | ArgumentNullException.ThrowIfNull(node);
26 |
27 | result = null;
28 |
29 | if (node is InvocationExpressionSyntax invocation &&
30 | invocation.Expression is MemberAccessExpressionSyntax accessor &&
31 | accessor.Expression is IdentifierNameSyntax identifierName &&
32 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) &&
33 | accessor.Name.Identifier.Text == "Plural")
34 | {
35 | var arguments = invocation.ArgumentList.Arguments;
36 | if (arguments.Count >= 2 &&
37 | arguments[1].GetExpression() is ArrayCreationExpressionSyntax array)
38 | {
39 | if (array.Type is PredefinedTypeSyntax arrayType &&
40 | arrayType.Keyword.Text == "String" &&
41 | array.Initializer.Initializers.Count >= 2 &&
42 | array.Initializer.Initializers.ElementAt(0) is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) &&
43 | array.Initializer.Initializers.ElementAt(1) is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression))
44 | {
45 |
46 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node);
47 |
48 | return true;
49 | }
50 | }
51 | else
52 | {
53 | if (arguments.Count >= 3 &&
54 | arguments[1].GetExpression() is LiteralExpressionSyntax singularLiteral && singularLiteral.IsKind(SyntaxKind.StringLiteralExpression) &&
55 | arguments[2].GetExpression() is LiteralExpressionSyntax pluralLiteral && pluralLiteral.IsKind(SyntaxKind.StringLiteralExpression))
56 | {
57 |
58 | result = CreateLocalizedString(singularLiteral.Token.ValueText, pluralLiteral.Token.ValueText, node);
59 |
60 | return true;
61 | }
62 | }
63 | }
64 |
65 | return false;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.VB/SingularStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.VisualBasic;
3 | using Microsoft.CodeAnalysis.VisualBasic.Syntax;
4 | using System;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB;
8 |
9 | ///
10 | /// Extracts with the singular text from the C# & VB AST node
11 | ///
12 | ///
13 | /// The localizable string is identified by the name convention - T["TEXT TO TRANSLATE"]
14 | ///
15 | ///
16 | /// Creates a new instance of a .
17 | ///
18 | /// The .
19 | public class SingularStringExtractor(IMetadataProvider metadataProvider) : LocalizableStringExtractor(metadataProvider)
20 | {
21 | ///
22 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result)
23 | {
24 | ArgumentNullException.ThrowIfNull(node);
25 |
26 | result = null;
27 |
28 | if (node is InvocationExpressionSyntax accessor &&
29 | accessor.Expression is IdentifierNameSyntax identifierName &&
30 | LocalizerAccessors.LocalizerIdentifiers.Contains(identifierName.Identifier.Text) &&
31 | accessor.ArgumentList != null)
32 | {
33 | var argument = accessor.ArgumentList.Arguments.FirstOrDefault();
34 | if (argument != null && argument.GetExpression() is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
35 | {
36 | result = CreateLocalizedString(literal.Token.ValueText, null, node);
37 | return true;
38 | }
39 | }
40 |
41 | return false;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet.VB/VisualBasicProjectProcessor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.VisualBasic;
2 | using OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders;
3 | using System;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB;
8 |
9 | ///
10 | /// Extracts localizable strings from all *.vb files in the project path.
11 | ///
12 | public class VisualBasicProjectProcessor : IProjectProcessor
13 | {
14 | private static readonly string _visualBasicExtension = "*.vb";
15 |
16 | ///
17 | public void Process(string path, string basePath, LocalizableStringCollection localizableStrings)
18 | {
19 | if (string.IsNullOrEmpty(path))
20 | {
21 | throw new ArgumentException($"'{nameof(path)}' cannot be null or empty.", nameof(path));
22 | }
23 |
24 | if (string.IsNullOrEmpty(basePath))
25 | {
26 | throw new ArgumentException($"'{nameof(basePath)}' cannot be null or empty.", nameof(basePath));
27 | }
28 |
29 | ArgumentNullException.ThrowIfNull(localizableStrings);
30 |
31 | var visualBasicMetadataProvider = new VisualBasicMetadataProvider(basePath);
32 | var visualBasicWalker = new ExtractingCodeWalker(
33 | [
34 | new SingularStringExtractor(visualBasicMetadataProvider),
35 | new PluralStringExtractor(visualBasicMetadataProvider),
36 | new ErrorMessageAnnotationStringExtractor(visualBasicMetadataProvider),
37 | new DisplayAttributeDescriptionStringExtractor(visualBasicMetadataProvider),
38 | new DisplayAttributeNameStringExtractor(visualBasicMetadataProvider),
39 | new DisplayAttributeGroupNameStringExtractor(visualBasicMetadataProvider),
40 | new DisplayAttributeShortNameStringExtractor(visualBasicMetadataProvider)
41 | ], localizableStrings);
42 |
43 | foreach (var file in Directory.EnumerateFiles(path, $"*{_visualBasicExtension}", SearchOption.AllDirectories).OrderBy(file => file))
44 | {
45 | using var stream = File.OpenRead(file);
46 | using var reader = new StreamReader(stream);
47 | var syntaxTree = VisualBasicSyntaxTree.ParseText(reader.ReadToEnd(), path: file);
48 |
49 | visualBasicWalker.Visit(syntaxTree.GetRoot());
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeDescriptionStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet;
5 |
6 | ///
7 | /// Extracts localizable string from Description property.
8 | ///
9 | ///
10 | /// Creates a new instance of a .
11 | ///
12 | /// The .
13 | public class DisplayAttributeDescriptionStringExtractor(IMetadataProvider metadataProvider)
14 | : DisplayAttributeStringExtractor("Description", metadataProvider)
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeGroupNameStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet;
5 |
6 | ///
7 | /// Extracts localizable string from GroupName property.
8 | ///
9 | ///
10 | /// Creates a new instance of a .
11 | ///
12 | /// The .
13 | public class DisplayAttributeGroupNameStringExtractor(IMetadataProvider metadataProvider)
14 | : DisplayAttributeStringExtractor("GroupName", metadataProvider)
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeNameStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet;
5 |
6 | ///
7 | /// Extracts localizable string from Name property.
8 | ///
9 | ///
10 | /// Creates a new instanceof a .
11 | ///
12 | /// The .
13 | public class DisplayAttributeNameStringExtractor(IMetadataProvider metadataProvider)
14 | : DisplayAttributeStringExtractor("Name", metadataProvider)
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeShortNameStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet;
5 |
6 | ///
7 | /// Extracts localizable string from ShortName property.
8 | ///
9 | ///
10 | /// Creates a new instance of a .
11 | ///
12 | /// The .
13 | public class DisplayAttributeShortNameStringExtractor(IMetadataProvider metadataProvider)
14 | : DisplayAttributeStringExtractor("ShortName", metadataProvider)
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/DisplayAttributeStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using System.ComponentModel.DataAnnotations;
5 |
6 | namespace OrchardCoreContrib.PoExtractor.DotNet;
7 |
8 | ///
9 | /// Extracts localizable string from .
10 | ///
11 | ///
12 | /// Creates a new instance of a .
13 | ///
14 | /// The argument name.
15 | /// The .
16 | public abstract class DisplayAttributeStringExtractor(string argumentName, IMetadataProvider metadataProvider)
17 | : LocalizableStringExtractor(metadataProvider)
18 | {
19 | private const string DisplayAttributeName = "Display";
20 |
21 | ///
22 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result)
23 | {
24 | result = null;
25 |
26 | if (node is AttributeArgumentSyntax argument
27 | && argument.Expression.Parent.ToFullString().StartsWith(argumentName)
28 | && node.Parent?.Parent is AttributeSyntax accessor
29 | && accessor.Name.ToString() == DisplayAttributeName
30 | && argument.Expression is LiteralExpressionSyntax literal
31 | && literal.IsKind(SyntaxKind.StringLiteralExpression))
32 | {
33 | result = CreateLocalizedString(literal.Token.ValueText, null, node);
34 | return true;
35 | }
36 |
37 | return false;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/ErrorMessageAnnotationStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using System;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.DotNet;
8 |
9 | ///
10 | /// Extracts localizable string from data annotations error messages.
11 | ///
12 | ///
13 | /// Creates a new instance of a .
14 | ///
15 | /// The .
16 | public class ErrorMessageAnnotationStringExtractor(IMetadataProvider metadataProvider)
17 | : LocalizableStringExtractor(metadataProvider)
18 | {
19 | private const string ErrorMessageAttributeName = "ErrorMessage";
20 |
21 | ///
22 | public override bool TryExtract(SyntaxNode node, out LocalizableStringOccurence result)
23 | {
24 | ArgumentNullException.ThrowIfNull(node, nameof(node));
25 |
26 | result = null;
27 |
28 | if (node is AttributeSyntax accessor && accessor.ArgumentList != null)
29 | {
30 | var argument = accessor.ArgumentList.Arguments
31 | .Where(a => a.Expression.Parent.ToFullString().StartsWith(ErrorMessageAttributeName))
32 | .FirstOrDefault();
33 |
34 | if (argument != null && argument.Expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
35 | {
36 | result = CreateLocalizedString(literal.Token.ValueText, null, node);
37 | return true;
38 | }
39 | }
40 |
41 | return false;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/ExtractingCodeWalker.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace OrchardCoreContrib.PoExtractor.DotNet;
6 |
7 | ///
8 | /// Traverses C# & VB AST and extracts localizable strings using provided collection of
9 | ///
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | /// the collection of extractors to use
14 | /// The where the results are saved.
15 | public class ExtractingCodeWalker(IEnumerable> extractors, LocalizableStringCollection strings) : SyntaxWalker
16 | {
17 | private readonly LocalizableStringCollection _strings = strings ?? throw new ArgumentNullException(nameof(strings));
18 | private readonly IEnumerable> _extractors = extractors ?? throw new ArgumentNullException(nameof(extractors));
19 |
20 | ///
21 | public override void Visit(SyntaxNode node)
22 | {
23 | ArgumentNullException.ThrowIfNull(node, nameof(node));
24 |
25 | base.Visit(node);
26 |
27 | foreach (var extractor in _extractors)
28 | {
29 | if (extractor.TryExtract(node, out var result))
30 | {
31 | _strings.Add(result);
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/LocalizerAccessors.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor.DotNet;
2 |
3 | ///
4 | /// Represents a class that contains a set of localizer identifier accessors.
5 | ///
6 | public static class LocalizerAccessors
7 | {
8 | ///
9 | /// Gets the localizer identifier for IStringLocalizer or IHtmlStringLocalizer in views.
10 | ///
11 | public static readonly string DefaultLocalizerIdentifier = "T";
12 |
13 | ///
14 | /// Gets the localizer identifier for IStringLocalizer.
15 | ///
16 | public static readonly string StringLocalizerIdentifier = "S";
17 |
18 | ///
19 | /// Gets the localizer identifier for IHtmlStringLocalizer.
20 | ///
21 | public static readonly string HtmlLocalizerIdentifier = "H";
22 |
23 | ///
24 | /// Gets the localizer identifiers.
25 | ///
26 | public static string[] LocalizerIdentifiers =
27 | [
28 | DefaultLocalizerIdentifier,
29 | StringLocalizerIdentifier,
30 | HtmlLocalizerIdentifier
31 | ];
32 | }
33 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/OrchardCoreContrib.PoExtractor.DotNet.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.DotNet/ProjectExtension.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor.DotNet
2 | {
3 | ///
4 | /// Represents a class that contains .NET projects extensions.
5 | ///
6 | public class ProjectExtension
7 | {
8 | ///
9 | /// Gets the CSharp project extension.
10 | ///
11 | public static readonly string CS = ".csproj";
12 |
13 | ///
14 | /// Gets the Visual Basic project extension.
15 | ///
16 | public static readonly string VB = ".vbproj";
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/ExtractingLiquidWalker.cs:
--------------------------------------------------------------------------------
1 | using Fluid.Ast;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace OrchardCoreContrib.PoExtractor.Liquid;
6 |
7 | ///
8 | /// Traverses Fluid AST and extracts localizable strings using provided collection of
9 | ///
10 | ///
11 | /// Initializes a new instance of the class
12 | ///
13 | /// the collection of extractors to use
14 | /// the where the results are saved
15 | public class ExtractingLiquidWalker(IEnumerable> extractors, LocalizableStringCollection localizableStrings)
16 | {
17 | private string _filePath;
18 |
19 | private readonly LocalizableStringCollection _localizableStrings = localizableStrings ?? throw new ArgumentNullException(nameof(localizableStrings));
20 | private readonly IEnumerable> _extractors = extractors ?? throw new ArgumentNullException(nameof(extractors));
21 |
22 | ///
23 | /// Visits liquid statement.
24 | ///
25 | /// The statement context.
26 | public void Visit(LiquidStatementContext statementContext)
27 | {
28 | ArgumentNullException.ThrowIfNull(statementContext);
29 |
30 | _filePath = statementContext.FilePath;
31 |
32 | Visit(statementContext.Statement);
33 | }
34 |
35 | private void Visit(Statement node)
36 | {
37 | switch (node)
38 | {
39 | case AssignStatement assign:
40 | Visit(assign.Value);
41 | break;
42 | case CaseStatement @case:
43 | Visit(@case.Statements);
44 | Visit(@case.Whens);
45 | Visit(@case.Else);
46 | Visit(@case.Expression);
47 | break;
48 | case CycleStatement cycle:
49 | Visit(cycle.Group);
50 | Visit(cycle.Values);
51 | break;
52 | case ElseIfStatement elseIf:
53 | Visit(elseIf.Condition);
54 | Visit(elseIf.Statements);
55 | break;
56 | case IfStatement @if:
57 | Visit(@if.Condition);
58 | Visit(@if.Statements);
59 | Visit(@if.ElseIfs);
60 | Visit(@if.Else);
61 | break;
62 | case OutputStatement output:
63 | Visit(output.Expression);
64 | Visit(output.Filters);
65 | break;
66 | case UnlessStatement unless:
67 | Visit(unless.Condition);
68 | Visit(unless.Statements);
69 | break;
70 | case WhenStatement @when:
71 | Visit(when.Options);
72 | Visit(when.Statements);
73 | break;
74 | case TagStatement tag:
75 | if (tag.Statements != null)
76 | {
77 | foreach (var item in tag.Statements)
78 | {
79 | Visit(item);
80 | }
81 | }
82 |
83 | break;
84 | }
85 | }
86 |
87 | private void Visit(IEnumerable statements)
88 | {
89 | if (statements == null)
90 | {
91 | return;
92 | }
93 |
94 | foreach (var statement in statements)
95 | {
96 | Visit(statement);
97 | }
98 | }
99 | private void Visit(Expression expression)
100 | {
101 | switch (expression)
102 | {
103 | case BinaryExpression binary:
104 | Visit(binary.Left);
105 | Visit(binary.Right);
106 | break;
107 | case FilterExpression filter:
108 | ProcessFilterExpression(filter);
109 | break;
110 |
111 | }
112 | }
113 |
114 | private void Visit(IEnumerable expressions)
115 | {
116 | if (expressions == null)
117 | {
118 | return;
119 | }
120 |
121 | foreach (var expression in expressions)
122 | {
123 | Visit(expression);
124 | }
125 | }
126 |
127 | private void ProcessFilterExpression(FilterExpression filter)
128 | {
129 | foreach (var extractor in _extractors)
130 | {
131 | if (extractor.TryExtract(new LiquidExpressionContext() { Expression = filter, FilePath = _filePath }, out var result))
132 | {
133 | _localizableStrings.Add(result);
134 | }
135 | }
136 |
137 | Visit(filter.Input);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidExpressionContext.cs:
--------------------------------------------------------------------------------
1 | using Fluid.Ast;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Liquid;
4 |
5 | ///
6 | /// Represents a liquid expression context.
7 | ///
8 | public class LiquidExpressionContext
9 | {
10 | ///
11 | /// Gets or sets the liquid file path.
12 | ///
13 | public string FilePath { get; set; }
14 |
15 | ///
16 | /// Gets or sets the expression.
17 | ///
18 | public FilterExpression Expression { get; set; }
19 | }
20 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidProjectProcessor.cs:
--------------------------------------------------------------------------------
1 | using Fluid;
2 | using Fluid.Parser;
3 | using Microsoft.Extensions.Options;
4 | using OrchardCore.DisplayManagement.Liquid;
5 | using OrchardCoreContrib.PoExtractor.Liquid.MetadataProviders;
6 | using System;
7 | using System.IO;
8 | using System.Linq;
9 |
10 | namespace OrchardCoreContrib.PoExtractor.Liquid;
11 |
12 | ///
13 | /// Extracts localizable strings from all *.liquid files in the project path
14 | ///
15 | public class LiquidProjectProcessor : IProjectProcessor
16 | {
17 | private static readonly string _liquidExtension = "*.liquid";
18 |
19 | private readonly LiquidViewParser _parser;
20 |
21 | ///
22 | /// Initializes a new instance of the
23 | ///
24 | public LiquidProjectProcessor()
25 | {
26 | var parserOptions = Options.Create(new LiquidViewOptions());
27 |
28 | _parser = new LiquidViewParser(parserOptions);
29 | }
30 |
31 | ///
32 | public void Process(string path, string basePath, LocalizableStringCollection localizableStrings)
33 | {
34 | ArgumentException.ThrowIfNullOrEmpty(path, nameof(path));
35 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath));
36 | ArgumentNullException.ThrowIfNull(localizableStrings);
37 |
38 | var liquidMetadataProvider = new LiquidMetadataProvider(basePath);
39 | var liquidVisitor = new ExtractingLiquidWalker([new LiquidStringExtractor(liquidMetadataProvider)], localizableStrings);
40 |
41 | foreach (var file in Directory.EnumerateFiles(path, $"*{_liquidExtension}", SearchOption.AllDirectories).OrderBy(file => file))
42 | {
43 | using var stream = File.OpenRead(file);
44 | using var reader = new StreamReader(stream);
45 | if (_parser.TryParse(reader.ReadToEnd(), out var template, out var errors))
46 | {
47 | ProcessTemplate(template, liquidVisitor, file);
48 | }
49 | }
50 | }
51 |
52 | private static void ProcessTemplate(IFluidTemplate template, ExtractingLiquidWalker visitor, string path)
53 | {
54 | if (template is CompositeFluidTemplate compositeTemplate)
55 | {
56 | foreach (var innerTemplate in compositeTemplate.Templates)
57 | {
58 | ProcessTemplate(innerTemplate, visitor, path);
59 | }
60 | }
61 | else if (template is FluidTemplate singleTemplate)
62 | {
63 | ProcessTemplate(singleTemplate, visitor, path);
64 | }
65 | }
66 |
67 | private static void ProcessTemplate(FluidTemplate template, ExtractingLiquidWalker visitor, string path)
68 | {
69 | foreach (var statement in template.Statements)
70 | {
71 | visitor.Visit(new LiquidStatementContext() { Statement = statement, FilePath = path });
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidStatementContext.cs:
--------------------------------------------------------------------------------
1 | using Fluid.Ast;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Liquid;
4 |
5 | ///
6 | /// Represents a liquid statement context.
7 | ///
8 | public class LiquidStatementContext
9 | {
10 | ///
11 | /// Gets or sets liquid file path.
12 | ///
13 | public string FilePath { get; set; }
14 |
15 | ///
16 | /// Gets or sets the liquid statement.
17 | ///
18 | public Statement Statement { get; set; }
19 | }
20 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/LiquidStringExtractor.cs:
--------------------------------------------------------------------------------
1 | using Fluid.Ast;
2 | using System;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.Liquid;
5 |
6 | ///
7 | /// Extracts localizable strings the Fluid AST node
8 | ///
9 | ///
10 | /// The localizable string is identified by the name convention of the filter - "TEXT TO TRANSLATE" | t
11 | ///
12 | ///
13 | /// Creates a new instance of a .
14 | ///
15 | /// The .
16 | public class LiquidStringExtractor(IMetadataProvider metadataProvider)
17 | : LocalizableStringExtractor(metadataProvider)
18 | {
19 | private static readonly string _localizationFilterName = "t";
20 |
21 | ///
22 | public override bool TryExtract(LiquidExpressionContext expressionContext, out LocalizableStringOccurence result)
23 | {
24 | ArgumentNullException.ThrowIfNull(expressionContext);
25 |
26 | result = null;
27 | var filter = expressionContext.Expression;
28 |
29 | if (filter.Name == _localizationFilterName)
30 | {
31 | if (filter.Input is LiteralExpression literal)
32 | {
33 | var text = literal
34 | .EvaluateAsync(new Fluid.TemplateContext())
35 | .GetAwaiter()
36 | .GetResult()
37 | .ToStringValue();
38 |
39 | result = CreateLocalizedString(text, null, expressionContext);
40 |
41 | return true;
42 | }
43 | }
44 |
45 | return false;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/MetadataProvider/LiquidMetadataProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.Liquid.MetadataProviders;
5 |
6 | ///
7 | /// Provides metadata for .liquid files.
8 | ///
9 | public class LiquidMetadataProvider : IMetadataProvider
10 | {
11 | private readonly string _basePath;
12 |
13 | ///
14 | /// Creates a new instance of a .
15 | ///
16 | /// The base path.
17 | public LiquidMetadataProvider(string basePath)
18 | {
19 | ArgumentException.ThrowIfNullOrEmpty(basePath, nameof(basePath));
20 |
21 | _basePath = basePath;
22 | }
23 |
24 | ///
25 | public string GetContext(LiquidExpressionContext expressionContext)
26 | {
27 | ArgumentNullException.ThrowIfNull(expressionContext, nameof(expressionContext));
28 |
29 | var path = expressionContext.FilePath.TrimStart(_basePath);
30 |
31 | return path.Replace(Path.DirectorySeparatorChar, '.').Replace(".liquid", string.Empty);
32 | }
33 |
34 | ///
35 | public LocalizableStringLocation GetLocation(LiquidExpressionContext expressionContext)
36 | {
37 | ArgumentNullException.ThrowIfNull(expressionContext, nameof(expressionContext));
38 |
39 | return new() { SourceFile = expressionContext.FilePath.TrimStart(_basePath) };
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Liquid/OrchardCoreContrib.PoExtractor.Liquid.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | OrchardCoreContrib.PoExtractor.Liquid
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Razor/MetadataProviders/RazorMetadataProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using System;
5 | using System.IO;
6 | using System.Linq;
7 |
8 | namespace OrchardCoreContrib.PoExtractor.Razor.MetadataProviders;
9 |
10 | ///
11 | /// Provides metadata for Razor .cshtml files.
12 | ///
13 | public class RazorMetadataProvider : IMetadataProvider
14 | {
15 | private static readonly string _razorPageExtension = ".cshtml";
16 | private static readonly string _razorComponentExtension = ".razor";
17 |
18 | private string[] _sourceCache;
19 | private string _sourceCachePath;
20 |
21 | private readonly string _basePath;
22 |
23 | ///
24 | /// Creates a new instance of a .
25 | ///
26 | /// The base path.
27 | public RazorMetadataProvider(string basePath)
28 | {
29 | _basePath = basePath;
30 | }
31 |
32 | ///
33 | public string GetContext(SyntaxNode node)
34 | {
35 | ArgumentNullException.ThrowIfNull(node);
36 |
37 | var path = node.SyntaxTree.FilePath.TrimStart(_basePath);
38 | path = RemoveRazorFileExtension(path);
39 |
40 | return path.Replace(Path.DirectorySeparatorChar, '.');
41 | }
42 |
43 | private static string RemoveRazorFileExtension(string path)
44 | {
45 | return path
46 | .Replace(_razorPageExtension, string.Empty)
47 | .Replace(_razorComponentExtension, string.Empty);
48 | }
49 |
50 | ///
51 | public LocalizableStringLocation GetLocation(SyntaxNode node)
52 | {
53 | ArgumentNullException.ThrowIfNull(node);
54 |
55 | var result = new LocalizableStringLocation
56 | {
57 | SourceFile = node.SyntaxTree.FilePath.TrimStart(_basePath)
58 | };
59 |
60 | var statement = node
61 | .Ancestors()
62 | .OfType()
63 | .FirstOrDefault();
64 |
65 | if (statement != null)
66 | {
67 | var lineTriviaSyntax = statement
68 | .DescendantTrivia()
69 | .OfType()
70 | .Where(o => o.IsKind(SyntaxKind.LineDirectiveTrivia) && o.HasStructure)
71 | .FirstOrDefault();
72 |
73 | if (lineTriviaSyntax.GetStructure() is LineDirectiveTriviaSyntax lineTrivia && lineTrivia.HashToken.Text == "#" && lineTrivia.DirectiveNameToken.Text == "line")
74 | {
75 | if (int.TryParse(lineTrivia.Line.Text, out var lineNumber))
76 | {
77 | result.SourceFileLine = lineNumber;
78 | result.Comment = GetSourceCodeLine(node.SyntaxTree.FilePath, lineNumber)?.Trim();
79 | }
80 | }
81 | }
82 |
83 | return result;
84 | }
85 |
86 | private string GetSourceCodeLine(string path, int line)
87 | {
88 | if (_sourceCachePath != path)
89 | {
90 | _sourceCache = null;
91 | _sourceCachePath = null;
92 |
93 | try
94 | {
95 | _sourceCache = File.ReadAllLines(path);
96 | _sourceCachePath = path;
97 | }
98 | catch
99 | {
100 | }
101 | }
102 |
103 | var zeroBasedLineNumber = line - 1;
104 | if (_sourceCache != null && _sourceCache.Length > zeroBasedLineNumber)
105 | {
106 | return _sourceCache[zeroBasedLineNumber];
107 | }
108 |
109 | return null;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Razor/OrchardCoreContrib.PoExtractor.Razor.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Razor/RazorPageGeneratorResult.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor.Razor
2 | {
3 | ///
4 | /// Represents a result for generated razor page.
5 | ///
6 | public class RazorPageGeneratorResult
7 | {
8 | ///
9 | /// Gets or sets the file path.
10 | ///
11 | public string FilePath { get; set; }
12 |
13 | ///
14 | /// Gets or sets the razor enerated code.
15 | ///
16 | public string GeneratedCode { get; set; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Razor/RazorProjectProcessor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using OrchardCoreContrib.PoExtractor.DotNet;
4 | using OrchardCoreContrib.PoExtractor.DotNet.CS;
5 | using OrchardCoreContrib.PoExtractor.Razor.MetadataProviders;
6 | using System;
7 |
8 | namespace OrchardCoreContrib.PoExtractor.Razor
9 | {
10 | ///
11 | /// Extracts localizable strings from all *.cshtml files in the folder Views under the project path
12 | ///
13 | public class RazorProjectProcessor : CSharpProjectProcessor
14 | {
15 | ///
16 | public override void Process(string path, string basePath, LocalizableStringCollection strings)
17 | {
18 | if (string.IsNullOrEmpty(path))
19 | {
20 | throw new ArgumentException($"'{nameof(path)}' cannot be null or empty.", nameof(path));
21 | }
22 |
23 | if (string.IsNullOrEmpty(basePath))
24 | {
25 | throw new ArgumentException($"'{nameof(basePath)}' cannot be null or empty.", nameof(basePath));
26 | }
27 |
28 | if (strings is null)
29 | {
30 | throw new ArgumentNullException(nameof(strings));
31 | }
32 |
33 | var razorMetadataProvider = new RazorMetadataProvider(basePath);
34 | var razorWalker = new ExtractingCodeWalker(new IStringExtractor[]
35 | {
36 | new SingularStringExtractor(razorMetadataProvider),
37 | new PluralStringExtractor(razorMetadataProvider),
38 | new ErrorMessageAnnotationStringExtractor(razorMetadataProvider),
39 | new DisplayAttributeDescriptionStringExtractor(razorMetadataProvider),
40 | new DisplayAttributeNameStringExtractor(razorMetadataProvider),
41 | new DisplayAttributeGroupNameStringExtractor(razorMetadataProvider),
42 | new DisplayAttributeShortNameStringExtractor(razorMetadataProvider)
43 | }, strings);
44 |
45 | var compiledViews = ViewCompiler.CompileViews(path);
46 |
47 | foreach (var view in compiledViews)
48 | {
49 | try
50 | {
51 | var syntaxTree = CSharpSyntaxTree.ParseText(view.GeneratedCode, path: view.FilePath);
52 |
53 | razorWalker.Visit(syntaxTree.GetRoot());
54 | }
55 | catch
56 | {
57 | Console.WriteLine("Process failed for: {0}", view.FilePath);
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor.Razor/ViewCompiler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Razor.Language;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace OrchardCoreContrib.PoExtractor.Razor
8 | {
9 | ///
10 | /// Represents a utility class to compile razor views.
11 | ///
12 | public static class ViewCompiler
13 | {
14 | ///
15 | /// Complies the views on a given project.
16 | ///
17 | /// The project directory.
18 | public static IEnumerable CompileViews(string projectDirectory)
19 | {
20 | if (string.IsNullOrEmpty(projectDirectory))
21 | {
22 | throw new ArgumentException($"'{nameof(projectDirectory)}' cannot be null or empty.", nameof(projectDirectory));
23 | }
24 |
25 | var projectEngine = CreateProjectEngine("OrchardCoreContrib.PoExtractor.GeneratedCode", projectDirectory);
26 |
27 | foreach (var item in projectEngine.FileSystem.EnumerateItems(projectDirectory).OrderBy(rzrProjItem => rzrProjItem.FileName))
28 | {
29 | yield return GenerateCodeFile(projectEngine, item);
30 | }
31 | }
32 |
33 | private static RazorProjectEngine CreateProjectEngine(string rootNamespace, string projectDirectory)
34 | {
35 | if (string.IsNullOrEmpty(rootNamespace))
36 | {
37 | throw new ArgumentException($"'{nameof(rootNamespace)}' cannot be null or empty.", nameof(rootNamespace));
38 | }
39 |
40 | if (string.IsNullOrEmpty(projectDirectory))
41 | {
42 | throw new ArgumentException($"'{nameof(projectDirectory)}' cannot be null or empty.", nameof(projectDirectory));
43 | }
44 |
45 | var fileSystem = RazorProjectFileSystem.Create(projectDirectory);
46 | var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder =>
47 | {
48 |
49 | builder
50 | .SetNamespace(rootNamespace)
51 | .ConfigureClass((document, @class) =>
52 | {
53 | @class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath);
54 | });
55 | #if NETSTANDARD2_0
56 | FunctionsDirective.Register(builder);
57 | InheritsDirective.Register(builder);
58 | SectionDirective.Register(builder);
59 | #endif
60 | });
61 |
62 | return projectEngine;
63 | }
64 |
65 | private static RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem)
66 | {
67 | var codeDocument = projectEngine.Process(projectItem);
68 | var cSharpDocument = codeDocument.GetCSharpDocument();
69 |
70 | return new RazorPageGeneratorResult
71 | {
72 | FilePath = projectItem.PhysicalPath,
73 | GeneratedCode = cSharpDocument.GeneratedCode,
74 | };
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/IgnoredProject.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace OrchardCoreContrib.PoExtractor;
4 |
5 | public class IgnoredProject
6 | {
7 | public static readonly string Docs = "src\\docs";
8 |
9 | public static readonly string Cms = "src\\OrchardCore.Cms.Web";
10 |
11 | public static readonly string Mvc = "src\\OrchardCore.Mvc.Web";
12 |
13 | public static readonly string Templates = "src\\Templates";
14 |
15 | public static readonly string Test = "test";
16 |
17 | private static readonly List _ignoredProjects = [ Docs, Cms, Mvc, Templates ];
18 |
19 | public static void Add(string project) => _ignoredProjects.Add(project);
20 |
21 | public static IEnumerable ToList() => _ignoredProjects;
22 | }
23 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/Language.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | public class Language
4 | {
5 | public static readonly string CSharp = "C#";
6 |
7 | public static readonly string VisualBasic = "VB";
8 | }
9 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/OrchardCoreContrib.PoExtractor.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | true
5 | OrchardCoreContrib.PoExtractor
6 | extractpo
7 | true
8 | 1.2.0
9 | The Orchard Core Contrib Team
10 |
11 | .NET Core global tool for extracting translatable strings from the OrchardCore source files.
12 | MIT
13 | https://github.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor
14 | https://github.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor
15 | git
16 | true
17 | Orchard Core, Orchard Core Contrib, Localization, PO
18 | https://github.com/OrchardCoreContrib/OrchardCoreContrib.PoExtractor/releases
19 | OrchardCoreContrib.PoExtractor
20 | icon.png
21 | Orchard Core Contrib Portable Object Extraction Tool
22 | true
23 | 2019 Orchard Core Contrib
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/PluginHelper.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp.Scripting;
2 | using Microsoft.CodeAnalysis.Scripting;
3 |
4 | namespace OrchardCoreContrib.PoExtractor;
5 |
6 | public static class PluginHelper
7 | {
8 | public static async Task ProcessPluginsAsync(
9 | IEnumerable plugins,
10 | List projectProcessors,
11 | List projectFiles)
12 | {
13 | var sharedOptions = ScriptOptions.Default.AddReferences(typeof(Program).Assembly);
14 |
15 | foreach (var plugin in plugins)
16 | {
17 | string code;
18 | ScriptOptions options;
19 |
20 | if (plugin.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
21 | {
22 | code = await new HttpClient().GetStringAsync(plugin);
23 | options = sharedOptions.WithFilePath(Path.Join(
24 | Environment.CurrentDirectory,
25 | Path.GetFileName(new Uri(plugin).AbsolutePath)));
26 | }
27 | else
28 | {
29 | code = await File.ReadAllTextAsync(plugin);
30 | options = sharedOptions.WithFilePath(Path.GetFullPath(plugin));
31 | }
32 |
33 | await CSharpScript.EvaluateAsync(code, options, new PluginContext(projectProcessors, projectFiles));
34 | }
35 | }
36 |
37 | public record PluginContext(List ProjectProcessors, List ProjectFiles);
38 | }
39 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/PoWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Text;
5 |
6 | namespace OrchardCoreContrib.PoExtractor;
7 |
8 | ///
9 | /// Writes objects in the Portable Object format to a stream
10 | ///
11 | public class PoWriter : IDisposable
12 | {
13 | public const string PortaleObjectTemplateExtension = ".pot";
14 |
15 | private readonly StreamWriter _writer;
16 |
17 | ///
18 | /// Creates a new instance of the , that writes records to the file
19 | ///
20 | /// the path to the file
21 | /// This function creates a new file or overwrites the existing file, if it already exists
22 | public PoWriter(string path)
23 | {
24 | _writer = new StreamWriter(File.Create(path));
25 | }
26 |
27 | ///
28 | /// Creates a new instance of the , that writes records to the stream
29 | ///
30 | ///
31 | public PoWriter(Stream stream)
32 | {
33 | _writer = new StreamWriter(stream);
34 | }
35 |
36 | ///
37 | /// Writes a object to the output
38 | ///
39 | /// the object to write
40 | public void WriteRecord(LocalizableString record)
41 | {
42 | foreach (var location in record.Locations)
43 | {
44 | _writer.WriteLine($"#: {location.SourceFile}:{location.SourceFileLine}");
45 |
46 | if (!string.IsNullOrEmpty(location.Comment))
47 | {
48 | _writer.WriteLine($"#. {location.Comment}");
49 | }
50 | }
51 |
52 | if (!string.IsNullOrEmpty(record.Context))
53 | {
54 | _writer.WriteLine($"msgctxt \"{Escape(record.Context)}\"");
55 | }
56 |
57 | _writer.WriteLine($"msgid \"{Escape(record.Text)}\"");
58 |
59 | if (string.IsNullOrEmpty(record.TextPlural))
60 | {
61 | _writer.WriteLine($"msgstr \"\"");
62 | }
63 | else
64 | {
65 | _writer.WriteLine($"msgid_plural \"{Escape(record.TextPlural)}\"");
66 | _writer.WriteLine($"msgstr[0] \"\"");
67 | }
68 |
69 |
70 | _writer.WriteLine();
71 | }
72 |
73 | ///
74 | /// Writes a collection of objects to the output
75 | ///
76 | /// the collection to write
77 | public void WriteRecord(IEnumerable records)
78 | {
79 | foreach (var record in records)
80 | {
81 | WriteRecord(record);
82 | }
83 | }
84 |
85 |
86 | ///
87 | public void Dispose()
88 | {
89 | Dispose(true);
90 | GC.SuppressFinalize(this);
91 | }
92 |
93 | protected virtual void Dispose(bool disposing)
94 | {
95 | if (disposing)
96 | {
97 | _writer.Close();
98 | _writer.Dispose();
99 | }
100 | }
101 |
102 | private static string Escape(string text)
103 | {
104 | var sb = new StringBuilder(text);
105 | sb.Replace("\\", "\\\\"); // \ -> \\
106 | sb.Replace("\"", "\\\""); // " -> \"
107 | sb.Replace("\r", "\\r");
108 | sb.Replace("\n", "\\n");
109 |
110 | return sb.ToString();
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/Program.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using McMaster.Extensions.CommandLineUtils;
3 | using OrchardCore.Modules;
4 | using OrchardCoreContrib.PoExtractor.DotNet;
5 | using OrchardCoreContrib.PoExtractor.DotNet.CS;
6 | using OrchardCoreContrib.PoExtractor.DotNet.VB;
7 | using OrchardCoreContrib.PoExtractor.Liquid;
8 | using OrchardCoreContrib.PoExtractor.Razor;
9 |
10 | namespace OrchardCoreContrib.PoExtractor;
11 |
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | var app = new CommandLineApplication();
17 |
18 | app.HelpOption();
19 |
20 | // Arguments
21 | var inputPath = app.Argument("Input Path", "The path to the input directory, all projects at the the path will be processed.")
22 | .IsRequired();
23 | var outputPath = app.Argument("Output Path", "The path to a directory where POT files will be generated.")
24 | .IsRequired();
25 |
26 | // Options
27 | var language = app.Option("-l|--language ", "Specifies the code language to extracts translatable strings from.", CommandOptionType.SingleValue, option =>
28 | {
29 | option.Accepts(cfg => cfg.Values("C#", "VB"));
30 | option.DefaultValue = "C#";
31 | });
32 | var template = app.Option("-t|--template ", "Specifies the template engine to extract the translatable strings from.", CommandOptionType.SingleValue, option =>
33 | option.Accepts(cfg => cfg.Values("Razor", "Liquid"))
34 | );
35 | var ignoredProjects = app.Option("-i|--ignore ", "Ignores extracting PO files from a given project(s).", CommandOptionType.MultipleValue);
36 | var localizers = app.Option("--localizer ", "Specifies the name of the localizer(s) that will be used during the extraction process.", CommandOptionType.MultipleValue);
37 | var single = app.Option("-s|--single ", "Specifies the single output file.", CommandOptionType.SingleValue);
38 | var plugins = app.Option(
39 | "-p|--plugin ",
40 | "A path or web URL with HTTPS scheme to a C# script (.csx) file which can define further " +
41 | "IProjectProcessor implementations. You can have multiple of this switch in a call.",
42 | CommandOptionType.MultipleValue,
43 | option => option.OnValidate(_ => option
44 | .Values
45 | .All(item => File.Exists(item) || item.StartsWithOrdinalIgnoreCase("https://"))
46 | ? ValidationResult.Success
47 | : new ValidationResult("Plugin must be an existing local file or a valid HTTPS URL.")));
48 |
49 | app.OnExecuteAsync(async cancellationToken =>
50 | {
51 | if (!Directory.Exists(inputPath.Value))
52 | {
53 | Console.WriteLine($"The input path '{inputPath.Value}' does not exist.");
54 |
55 | return;
56 | }
57 |
58 | foreach (var ignoredProject in ignoredProjects.Values)
59 | {
60 | IgnoredProject.Add(ignoredProject);
61 | }
62 |
63 | LocalizerAccessors.LocalizerIdentifiers = [.. localizers.Values];
64 |
65 | var projectFiles = new List();
66 | var projectProcessors = new List();
67 |
68 | if (language.Value() == Language.CSharp)
69 | {
70 | projectProcessors.Add(new CSharpProjectProcessor());
71 |
72 | projectFiles.AddRange(Directory
73 | .EnumerateFiles(inputPath.Value, $"*{ProjectExtension.CS}", SearchOption.AllDirectories)
74 | .OrderBy(f => f));
75 | }
76 | else
77 | {
78 | projectProcessors.Add(new VisualBasicProjectProcessor());
79 |
80 | projectFiles.AddRange(Directory
81 | .EnumerateFiles(inputPath.Value, $"*{ProjectExtension.VB}", SearchOption.AllDirectories)
82 | .OrderBy(f => f));
83 | }
84 |
85 | if (template.Value() == TemplateEngine.Both)
86 | {
87 | projectProcessors.Add(new RazorProjectProcessor());
88 | projectProcessors.Add(new LiquidProjectProcessor());
89 | }
90 | else if (template.Value() == TemplateEngine.Razor)
91 | {
92 | projectProcessors.Add(new RazorProjectProcessor());
93 | }
94 | else if (template.Value() == TemplateEngine.Liquid)
95 | {
96 | projectProcessors.Add(new LiquidProjectProcessor());
97 | }
98 |
99 | if (plugins.Values.Count > 0)
100 | {
101 | await PluginHelper.ProcessPluginsAsync(plugins.Values, projectProcessors, projectFiles);
102 | }
103 |
104 | var isSingleFileOutput = !string.IsNullOrEmpty(single.Value());
105 | var localizableStrings = new LocalizableStringCollection();
106 | foreach (var projectFile in projectFiles)
107 | {
108 | var projectPath = Path.GetDirectoryName(projectFile);
109 | var projectBasePath = Path.GetDirectoryName(projectPath) + Path.DirectorySeparatorChar;
110 | var projectRelativePath = projectPath[projectBasePath.Length..];
111 | var rootedProject = projectPath == inputPath.Value
112 | ? projectPath
113 | : projectPath[(projectPath.IndexOf(inputPath.Value, StringComparison.Ordinal) + inputPath.Value.Length + 1)..];
114 | if (IgnoredProject.ToList().Any(p => rootedProject.StartsWith(p)))
115 | {
116 | continue;
117 | }
118 |
119 | foreach (var projectProcessor in projectProcessors)
120 | {
121 | projectProcessor.Process(projectPath, projectBasePath, localizableStrings);
122 | }
123 |
124 | if (!isSingleFileOutput)
125 | {
126 | if (localizableStrings.Values.Any())
127 | {
128 | var potPath = Path.Combine(outputPath.Value, Path.GetFileNameWithoutExtension(projectFile) + PoWriter.PortaleObjectTemplateExtension);
129 |
130 | Directory.CreateDirectory(Path.GetDirectoryName(potPath));
131 |
132 | using var potFile = new PoWriter(potPath);
133 | potFile.WriteRecord(localizableStrings.Values);
134 | }
135 |
136 | Console.WriteLine($"{Path.GetFileName(projectPath)}: Found {localizableStrings.Values.Count()} strings.");
137 | localizableStrings.Clear();
138 | }
139 | }
140 |
141 | if (isSingleFileOutput)
142 | {
143 | if (localizableStrings.Values.Any())
144 | {
145 | var potPath = Path.Combine(outputPath.Value, single.Value());
146 |
147 | Directory.CreateDirectory(Path.GetDirectoryName(potPath));
148 |
149 | using var potFile = new PoWriter(potPath);
150 | potFile.WriteRecord(localizableStrings.Values);
151 | }
152 |
153 | Console.WriteLine($"Found {localizableStrings.Values.Count()} strings.");
154 | }
155 | });
156 |
157 | app.Execute(args);
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/OrchardCoreContrib.PoExtractor/TemplateEngine.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor;
2 |
3 | public class TemplateEngine
4 | {
5 | public static readonly string Liquid = "Liquid";
6 |
7 | public static readonly string Razor = "Razor";
8 |
9 | public static readonly string Both = string.Empty;
10 | }
11 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/DisplayAttributeStringExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using OrchardCoreContrib.PoExtractor.Tests.Fakes;
2 | using System.Linq;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.Tests;
5 |
6 | public class DisplayAttributeStringExtractorTests
7 | {
8 | private readonly FakeCSharpProjectProcessor _fakeCSharpProjectProcessor = new();
9 |
10 | [Fact]
11 | public void ExtractLocalizedNameFromDisplayAttribute()
12 | {
13 | // Arrange
14 | var localizableStringCollection = new LocalizableStringCollection();
15 |
16 | // Act
17 | _fakeCSharpProjectProcessor.Process(string.Empty, string.Empty, localizableStringCollection);
18 |
19 | // Assert
20 | var localizedStrings = localizableStringCollection.Values
21 | .Select(s => s.Text)
22 | .ToList();
23 |
24 | Assert.Contains(localizedStrings, s => s == "First name");
25 | }
26 |
27 | [Fact]
28 | public void ExtractLocalizedShortNameFromDisplayAttribute()
29 | {
30 | // Arrange
31 | var localizableStringCollection = new LocalizableStringCollection();
32 |
33 | // Act
34 | _fakeCSharpProjectProcessor.Process(string.Empty, string.Empty, localizableStringCollection);
35 |
36 | // Assert
37 | var localizedStrings = localizableStringCollection.Values
38 | .Select(s => s.Text)
39 | .ToList();
40 |
41 | Assert.Contains(localizedStrings, s => s == "1st name");
42 | }
43 |
44 | [Fact]
45 | public void ExtractLocalizedGroupNameFromDisplayAttribute()
46 | {
47 | // Arrange
48 | var localizableStringCollection = new LocalizableStringCollection();
49 |
50 | // Act
51 | _fakeCSharpProjectProcessor.Process(string.Empty, string.Empty, localizableStringCollection);
52 |
53 | // Assert
54 | var localizedStrings = localizableStringCollection.Values
55 | .Select(s => s.Text)
56 | .ToList();
57 |
58 | Assert.Contains(localizedStrings, s => s == "Person info");
59 | }
60 |
61 | [Fact]
62 | public void ExtractLocalizedDescriptionFromDisplayAttribute()
63 | {
64 | // Arrange
65 | var localizableStringCollection = new LocalizableStringCollection();
66 |
67 | // Act
68 | _fakeCSharpProjectProcessor.Process(string.Empty, string.Empty, localizableStringCollection);
69 |
70 | // Assert
71 | var localizedStrings = localizableStringCollection.Values
72 | .Select(s => s.Text)
73 | .ToList();
74 |
75 | Assert.Contains(localizedStrings, s => s == "The first name of the person");
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/ErrorMessageAnnotationStringExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using OrchardCoreContrib.PoExtractor.Tests.Fakes;
2 | using System.Linq;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.Tests;
5 |
6 | public class ErrorMessageAnnotationStringExtractorTests
7 | {
8 | private readonly FakeCSharpProjectProcessor _fakeCSharpProjectProcessor = new();
9 |
10 | [Fact]
11 | public void ExtractLocalizedStringsFromDataAnnotations()
12 | {
13 | // Arrange
14 | var localizableStringCollection = new LocalizableStringCollection();
15 |
16 | // Act
17 | _fakeCSharpProjectProcessor.Process(string.Empty, string.Empty, localizableStringCollection);
18 |
19 | // Assert
20 | var localizedStrings = localizableStringCollection.Values
21 | .Select(s => s.Text)
22 | .ToList();
23 |
24 | Assert.NotEmpty(localizedStrings);
25 | Assert.Equal(6, localizedStrings.Count);
26 | Assert.Contains(localizedStrings, s => s == "The username is required.");
27 | }
28 |
29 | [Fact]
30 | public void DataAnnotationsExtractorShouldRespectErrorMessageOrder()
31 | {
32 | // Arrange
33 | var localizableStringCollection = new LocalizableStringCollection();
34 |
35 | // Act
36 | _fakeCSharpProjectProcessor.Process(string.Empty, string.Empty, localizableStringCollection);
37 |
38 | // Assert
39 | var localizedStrings = localizableStringCollection.Values
40 | .Select(s => s.Text)
41 | .ToList();
42 |
43 | Assert.Contains(localizedStrings, s => s == "Age should be in the range [15-45].");
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/Extensions/StringExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor.Tests.Extensions;
2 |
3 | public class StringExtensionsTests
4 | {
5 | [Theory]
6 | [InlineData("TEST-some-other-content-TEST", "TEST", "-some-other-content-TEST")]
7 | [InlineData(
8 | @"D:\Repositories\OrchardCoreContrib.PoExtractor\src\WebApplication1\WebApplication1\Pages\Index.cshtml",
9 | @"D:\Repositories\OrchardCoreContrib.PoExtractor\src\WebApplication1\",
10 | "WebApplication1\\Pages\\Index.cshtml")]
11 | public void TrimStart_TrimsTextFromStartOfString(string text, string textToBeRemoved, string expected)
12 | {
13 | // Act
14 | var result = text.TrimStart(textToBeRemoved);
15 |
16 | // Assert
17 | Assert.Equal(expected, result);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/Fakes/FakeCSharpProjectProcessor.cs:
--------------------------------------------------------------------------------
1 | using OrchardCoreContrib.PoExtractor.DotNet.CS;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Tests.Fakes;
4 |
5 | public class FakeCSharpProjectProcessor : IProjectProcessor
6 | {
7 | private static readonly string _defaultPath = "ProjectFiles";
8 |
9 | public void Process(string path, string basePath, LocalizableStringCollection localizableStrings)
10 | {
11 | if (string.IsNullOrEmpty(path))
12 | {
13 | path = _defaultPath;
14 | }
15 |
16 | if (string.IsNullOrEmpty(basePath))
17 | {
18 | basePath = _defaultPath;
19 | }
20 |
21 | var csharpProjectProcessor = new CSharpProjectProcessor();
22 |
23 | csharpProjectProcessor.Process(path, basePath, localizableStrings);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/LocalizableStringCollectionTests.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor.Tests;
2 |
3 | public class LocalizableStringCollectionTests
4 | {
5 | private readonly LocalizableStringOccurence _localizedString1 = new()
6 | {
7 | Text = "Computer",
8 | Location = new LocalizableStringLocation() { SourceFileLine = 1 }
9 | };
10 | private readonly LocalizableStringOccurence _localizedString2 = new()
11 | {
12 | Text = "Computer",
13 | Location = new LocalizableStringLocation() { SourceFileLine = 1 }
14 | };
15 | private readonly LocalizableStringOccurence _localizedString3 = new()
16 | {
17 | Text = "Keyboard", Location = new LocalizableStringLocation() { SourceFileLine = 1 }
18 | };
19 |
20 | [Fact]
21 | public void WhenCreated_ValuesCollectionIsEmpty()
22 | {
23 | // Arrange
24 | var localizableString = new LocalizableStringCollection();
25 |
26 | // Assert
27 | Assert.Empty(localizableString.Values);
28 | }
29 |
30 | [Fact]
31 | public void Add_CreatesNewLocalizableString_IfTheCollectionIsEmpty()
32 | {
33 | // Arrange
34 | var localizableString = new LocalizableStringCollection();
35 |
36 | // Act
37 | localizableString.Add(_localizedString1);
38 |
39 | // Assert
40 | var result = Assert.Single(localizableString.Values);
41 | Assert.Equal(_localizedString1.Text, result.Text);
42 | Assert.Contains(_localizedString1.Location, result.Locations);
43 | }
44 |
45 | [Fact]
46 | public void Add_AddsLocationToLocalizableString_IfTheCollectionContainsSameString()
47 | {
48 | // Arrange
49 | var localizableString = new LocalizableStringCollection();
50 |
51 | // Act
52 | localizableString.Add(_localizedString1);
53 | localizableString.Add(_localizedString2);
54 |
55 | // Assert
56 | var result = Assert.Single(localizableString.Values);
57 | Assert.Equal(_localizedString1.Text, result.Text);
58 | Assert.Contains(_localizedString1.Location, result.Locations);
59 | Assert.Contains(_localizedString2.Location, result.Locations);
60 | }
61 |
62 | [Fact]
63 | public void Add_CreatesNewLocalizableString_IfTheCollectionDoesntContainSameString()
64 | {
65 | // Arrange
66 | var localizableString = new LocalizableStringCollection();
67 |
68 | // Act
69 | localizableString.Add(_localizedString1);
70 | localizableString.Add(_localizedString3);
71 |
72 | // Assert
73 | Assert.Contains(localizableString.Values, o => o.Text == _localizedString1.Text);
74 | Assert.Contains(localizableString.Values, o => o.Text == _localizedString2.Text);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/LocalizableStringTests.cs:
--------------------------------------------------------------------------------
1 | namespace OrchardCoreContrib.PoExtractor.Tests;
2 |
3 | public class LocalizableStringTests
4 | {
5 | [Fact]
6 | public void Constructor_PopulatesProperties()
7 | {
8 | // Arrange
9 | var source = new LocalizableStringOccurence()
10 | {
11 | Context = "OrchardCoreContrib.PoExtractor",
12 | Text = "Computer",
13 | TextPlural = "Computers",
14 | Location = new LocalizableStringLocation()
15 | {
16 | Comment = "Comment",
17 | SourceFile = "Test.cs",
18 | SourceFileLine = 1
19 | }
20 | };
21 |
22 | // Act
23 | var result = new LocalizableString(source);
24 |
25 | // Assert
26 | Assert.Equal(source.Context, result.Context);
27 | Assert.Equal(source.Text, result.Text);
28 | Assert.Equal(source.TextPlural, result.TextPlural);
29 | Assert.Single(result.Locations, source.Location);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/OrchardCoreContrib.PoExtractor.Abstractions.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | OrchardCoreContrib.PoExtractor.Tests
4 |
5 |
6 |
7 |
8 |
9 |
10 | all
11 | runtime; build; native; contentfiles; analyzers
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | PreserveNewest
20 |
21 |
22 | PreserveNewest
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/ProjectFiles/LoginViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Tests.Files;
4 |
5 | public class LoginViewModel
6 | {
7 | [Required(ErrorMessage = "The username is required.")]
8 | public string UserName { get; set; }
9 |
10 | [Required]
11 | public string Password { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/ProjectFiles/PersonModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Tests.ProjectFiles;
4 |
5 | public class PersonModel
6 | {
7 | [Display(Name = "First name", ShortName = "1st name", Description = "The first name of the person", GroupName = "Person info")]
8 | public string FirstName { get; set; }
9 |
10 | public string LastName { get; set; }
11 |
12 | [Range(15, 45, ErrorMessage = "Age should be in the range [15-45].")]
13 | public int Age { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Core.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests/MetadataProviders/CSharpMetadataProviderTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders.Tests;
5 |
6 | public class CSharpMetadataProviderTests
7 | {
8 | [Fact]
9 | public void ContextShouldNotIgnoreInnerClass()
10 | {
11 | // Arrange
12 | var metadataProvider = new CSharpMetadataProvider("DummyBasePath");
13 | var codeText = @"namespace OrchardCoreContrib.PoExtractor.Tests
14 |
15 | public class TestClass
16 | {
17 | public InnerClass Model { get; set; }
18 |
19 | public class InnerClass
20 | {
21 | [Display(Name = ""Remember me?"")]
22 | public bool RememberMe { get; set; }
23 | }
24 | }";
25 |
26 | var syntaxTree = CSharpSyntaxTree.ParseText(codeText, path: "DummyPath");
27 |
28 | var node = syntaxTree
29 | .GetRoot()
30 | .DescendantNodes()
31 | .OfType()
32 | .Last()
33 | .ChildNodes()
34 | .First();
35 |
36 | // Act
37 | var context = metadataProvider.GetContext(node);
38 |
39 | // Assert
40 | Assert.Equal("OrchardCoreContrib.PoExtractor.Tests.TestClass.InnerClass", context);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | runtime; build; native; contentfiles; analyzers; buildtransitive
8 | all
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests/PluralStringExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS.Tests;
5 |
6 | public class PluralStringExtractorTests
7 | {
8 | [Fact]
9 | public void ShouldExtractValidString()
10 | {
11 | // Arrange
12 | var text = "{0} thing";
13 | var pluralText = "{0} things";
14 | var metadataProvider = new CSharpMetadataProvider("DummyBasePath");
15 | var extractor = new PluralStringExtractor(metadataProvider);
16 |
17 | var syntaxTree = CSharpSyntaxTree
18 | .ParseText($"S.Plural(1, \"{text}\", \"{pluralText}\");", path: "DummyPath");
19 |
20 | var node = syntaxTree
21 | .GetRoot()
22 | .DescendantNodes()
23 | .ElementAt(2);
24 |
25 | // Act
26 | var extracted = extractor.TryExtract(node, out var result);
27 |
28 | // Assert
29 | Assert.True(extracted);
30 | Assert.Equal(text, result.Text);
31 | Assert.Equal(pluralText, result.TextPlural);
32 | }
33 | }
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests/SingularStringExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using OrchardCoreContrib.PoExtractor.DotNet.CS.MetadataProviders;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet.CS.Tests;
5 |
6 | public class SingularStringExtractorTests
7 | {
8 | [Fact]
9 | public void ExtractString()
10 | {
11 | // Arrange
12 | var text = "Thing";
13 | var metadataProvider = new CSharpMetadataProvider("DummyBasePath");
14 | var extractor = new SingularStringExtractor(metadataProvider);
15 |
16 | var syntaxTree = CSharpSyntaxTree.ParseText($"S[\"{text}\"];", path: "DummyPath");
17 |
18 | var node = syntaxTree
19 | .GetRoot()
20 | .DescendantNodes()
21 | .ElementAt(2);
22 |
23 | // Act
24 | var extracted = extractor.TryExtract(node, out var result);
25 |
26 | // Assert
27 | Assert.True(extracted);
28 | Assert.Equal(text, result.Text);
29 | }
30 | }
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.CS.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.VB.Tests/MetadataProviders/VisualBasicMetadataProviderTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.VisualBasic;
2 | using Microsoft.CodeAnalysis.VisualBasic.Syntax;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders.Tests;
5 |
6 | public class VisualBasicMetadataProviderTests
7 | {
8 | [Fact]
9 | public void ContextShouldNotIgnoreInnerClass()
10 | {
11 | // Arrange
12 | var metadataProvider = new VisualBasicMetadataProvider("DummyBasePath");
13 | var codeText = @"Namespace OrchardCoreContrib.PoExtractor.Tests
14 | Public Class TestClass
15 | Public Property Model As InnerClass
16 |
17 | Public class InnerClass
18 |
19 | Public Property RememberMe As Boolean
20 | End Class
21 | End Class
22 | End Namespace";
23 |
24 | var syntaxTree = VisualBasicSyntaxTree.ParseText(codeText, path: "DummyPath");
25 |
26 | var node = syntaxTree
27 | .GetRoot()
28 | .DescendantNodes()
29 | .OfType()
30 | .Last()
31 | .ChildNodes()
32 | .First();
33 |
34 | // Act
35 | var context = metadataProvider.GetContext(node);
36 |
37 | // Assert
38 | Assert.Equal("OrchardCoreContrib.PoExtractor.Tests.TestClass.InnerClass", context);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.VB.Tests/OrchardCoreContrib.PoExtractor.DotNet.VB.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | runtime; build; native; contentfiles; analyzers; buildtransitive
8 | all
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.VB.Tests/PluralStringExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.VisualBasic;
2 | using OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB.Tests;
5 |
6 | public class PluralStringExtractorTests
7 | {
8 | [Fact]
9 | public void ShouldExtractValidString()
10 | {
11 | // Arrange
12 | var text = "{0} thing";
13 | var pluralText = "{0} things";
14 | var metadataProvider = new VisualBasicMetadataProvider("DummyBasePath");
15 | var extractor = new PluralStringExtractor(metadataProvider);
16 |
17 | var syntaxTree = VisualBasicSyntaxTree
18 | .ParseText($"S.Plural(1, \"{text}\", \"{pluralText}\")", path: "DummyPath");
19 |
20 | var node = syntaxTree
21 | .GetRoot()
22 | .DescendantNodes()
23 | .ElementAt(1);
24 |
25 | // Act
26 | var extracted = extractor.TryExtract(node, out var result);
27 |
28 | // Assert
29 | Assert.True(extracted);
30 | Assert.Equal(text, result.Text);
31 | Assert.Equal(pluralText, result.TextPlural);
32 | }
33 | }
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.VB.Tests/SingularStringExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.VisualBasic;
2 | using OrchardCoreContrib.PoExtractor.DotNet.VB.MetadataProviders;
3 |
4 | namespace OrchardCoreContrib.PoExtractor.DotNet.VB.Tests;
5 |
6 | public class SingularStringExtractorTests
7 | {
8 | [Fact]
9 | public void ExtractString()
10 | {
11 | // Arrange
12 | var text = "Thing";
13 | var metadataProvider = new VisualBasicMetadataProvider("DummyBasePath");
14 | var extractor = new SingularStringExtractor(metadataProvider);
15 |
16 | var syntaxTree = VisualBasicSyntaxTree.ParseText($"S(\"{text}\")", path: "DummyPath");
17 |
18 | var node = syntaxTree
19 | .GetRoot()
20 | .DescendantNodes()
21 | .ElementAt(1);
22 |
23 | // Act
24 | var extracted = extractor.TryExtract(node, out var result);
25 |
26 | // Assert
27 | Assert.True(extracted);
28 | Assert.Equal(text, result.Text);
29 | }
30 | }
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.DotNet.VB.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Liquid.Tests/LiquidProjectProcessorTests.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Liquid.Tests;
4 |
5 | public class LiquidProjectProcessorTests
6 | {
7 | private readonly LiquidProjectProcessor _processor = new();
8 | private readonly LocalizableStringCollection _localizableStrings = new();
9 |
10 | [Fact]
11 | public void ExtractsStringFromLiquidProperty()
12 | {
13 | // Act
14 | _processor.Process("ProjectFiles", "DummyBasePath", _localizableStrings);
15 |
16 | // Assert
17 | Assert.Contains(_localizableStrings.Values, s => s.Text == "string in variable");
18 | }
19 |
20 | [Fact]
21 | public void ExtractsStringFromLiquidExpression()
22 | {
23 | // Act
24 | _processor.Process("ProjectFiles", "DummyBasePath", _localizableStrings);
25 |
26 | // Assert
27 | Assert.Contains(_localizableStrings.Values, s => s.Text == "string in expression");
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Liquid.Tests/OrchardCoreContrib.PoExtractor.Liquid.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | runtime; build; native; contentfiles; analyzers; buildtransitive
8 | all
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | PreserveNewest
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Liquid.Tests/ProjectFiles/sample.liquid:
--------------------------------------------------------------------------------
1 | {{ "string in expression" | t }}
2 | {% assign mystring = "string in variable" | t %}
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Tests/OrchardCoreContrib.PoExtractor.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | runtime; build; native; contentfiles; analyzers; buildtransitive
8 | all
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | PreserveNewest
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/BasicJsonLocalizationProcessor.csx:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Text.Json.Nodes;
5 | using OrchardCoreContrib.PoExtractor;
6 |
7 | // This example plugin implements processing for a very simplistic subset of the i18next JSON format. It only supports
8 | // strings and other objects, and the files must be located in i18n/{language}.json. Even though this is only meant as a
9 | // demo, even this much can be useful in a real life scenario if paired with a backend API that generates the files for
10 | // other languages using PO files, to centralize the localization tooling.
11 | public class BasicJsonLocalizationProcessor : IProjectProcessor
12 | {
13 | public void Process(string path, string basePath, LocalizableStringCollection strings)
14 | {
15 | ArgumentException.ThrowIfNullOrEmpty(path);
16 | ArgumentException.ThrowIfNullOrEmpty(basePath);
17 | ArgumentNullException.ThrowIfNull(strings);
18 |
19 | var jsonFilePaths = Directory.GetFiles(path, "*.json", SearchOption.AllDirectories)
20 | .Where(path => Path.GetFileNameWithoutExtension(path).ToUpperInvariant() is "EN" or "00" or "IV")
21 | .Where(path => Path.GetFileName(Path.GetDirectoryName(path))?.ToUpperInvariant() is "I18N")
22 | .GroupBy(Path.GetDirectoryName)
23 | .Select(group => group
24 | .OrderBy(path => Path.GetFileNameWithoutExtension(path).ToUpperInvariant() switch
25 | {
26 | "EN" => 0,
27 | "00" => 1,
28 | "IV" => 2,
29 | _ => 3,
30 | })
31 | .ThenBy(path => path)
32 | .First());
33 |
34 | foreach (var jsonFilePath in jsonFilePaths)
35 | {
36 | try
37 | {
38 | ProcessJson(
39 | jsonFilePath,
40 | strings,
41 | JObject.Parse(File.ReadAllText(jsonFilePath)),
42 | string.Empty);
43 | }
44 | catch
45 | {
46 | Console.WriteLine("Process failed for: {0}", path);
47 | }
48 | }
49 | }
50 |
51 | private static void ProcessJson(string path, LocalizableStringCollection strings, JsonNode json, string prefix)
52 | {
53 | if (json is JsonObject jsonObject)
54 | {
55 | foreach (var (name, value) in jsonObject)
56 | {
57 | var newPrefix = string.IsNullOrEmpty(prefix) ? name : $"{prefix}.{name}";
58 | ProcessJson(path, strings, value, newPrefix);
59 | }
60 |
61 | return;
62 | }
63 |
64 | if (json is JsonValue jsonValue)
65 | {
66 | var value = jsonValue.GetObjectValue()?.ToString();
67 | strings.Add(new()
68 | {
69 | Context = prefix,
70 | Location = new() { SourceFile = path },
71 | Text = value,
72 | });
73 | }
74 | }
75 | }
76 |
77 | ProjectProcessors.Add(new BasicJsonLocalizationProcessor());
78 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Tests/PluginTestFiles/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "about": {
3 | "title": "About us",
4 | "notes": "Title for main menu"
5 | },
6 | "home": {
7 | "title": "Home page",
8 | "context": "Displayed on the main website page"
9 | },
10 | "admin.login": {
11 | "title": "Administrator login"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Tests/PluginTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Text.RegularExpressions;
3 | using Xunit;
4 |
5 | namespace OrchardCoreContrib.PoExtractor.Tests;
6 |
7 | public partial class PluginTests
8 | {
9 | private const string PluginTestFiles = nameof(PluginTestFiles);
10 |
11 | [Fact]
12 | public async Task ProcessPluginsBasicJsonLocalizationProcessor()
13 | {
14 | // Arrange
15 | using var stream = new MemoryStream();
16 | var plugins = new[] { Path.Join(PluginTestFiles, "BasicJsonLocalizationProcessor.csx") };
17 | var projectProcessors = new List();
18 | var projectFiles = new List { Path.Join(PluginTestFiles, "OrchardCoreContrib.PoExtractor.Tests.dll") };
19 | var localizableStrings = new LocalizableStringCollection();
20 |
21 | // Act
22 | await PluginHelper.ProcessPluginsAsync(plugins, projectProcessors, projectFiles);
23 | projectProcessors[0].Process(PluginTestFiles, Path.GetFileName(Environment.CurrentDirectory), localizableStrings);
24 |
25 | using (var writer = new PoWriter(stream))
26 | {
27 | writer.WriteRecord(localizableStrings.Values);
28 | }
29 |
30 | // Assert
31 | Assert.Single(projectProcessors);
32 | Assert.Single(projectFiles);
33 | Assert.Equal(5, localizableStrings.Values.Count());
34 |
35 | const string expectedResult = @"
36 | #: PluginTestFiles/i18n/en.json:0
37 | msgctxt ""about.title""
38 | msgid ""About us""
39 | msgstr """"
40 |
41 | #: PluginTestFiles/i18n/en.json:0
42 | msgctxt ""about.notes""
43 | msgid ""Title for main menu""
44 | msgstr """"
45 |
46 | #: PluginTestFiles/i18n/en.json:0
47 | msgctxt ""home.title""
48 | msgid ""Home page""
49 | msgstr """"
50 |
51 | #: PluginTestFiles/i18n/en.json:0
52 | msgctxt ""home.context""
53 | msgid ""Displayed on the main website page""
54 | msgstr """"
55 |
56 | #: PluginTestFiles/i18n/en.json:0
57 | msgctxt ""admin.login.title""
58 | msgid ""Administrator login""
59 | msgstr """"";
60 | var actualResult = Encoding.UTF8.GetString(stream.ToArray());
61 | Assert.Equal(CleanupSpaces(expectedResult), CleanupSpaces(actualResult));
62 | }
63 |
64 | private static string CleanupSpaces(string input)
65 | {
66 | // Trim leading whitespaces.
67 | input = TrimLeadingSpacesRegex().Replace(input.Trim(), string.Empty);
68 |
69 | // Make the path OS-specific, so the test works on Windows as well.
70 | return input.Replace('/', Path.DirectorySeparatorChar);
71 | }
72 |
73 | [GeneratedRegex(@"^\s+", RegexOptions.Multiline)]
74 | private static partial Regex TrimLeadingSpacesRegex();
75 | }
76 |
--------------------------------------------------------------------------------
/test/OrchardCoreContrib.PoExtractor.Tests/PoWriterTests.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | namespace OrchardCoreContrib.PoExtractor.Tests;
4 |
5 | public class PoWriterTests
6 | {
7 | private readonly MemoryStream _stream = new();
8 |
9 | [Fact]
10 | public void WriteRecord_WritesSingularLocalizableString()
11 | {
12 | // Arrange
13 | var localizableString = new LocalizableString
14 | {
15 | Text = "Computer"
16 | };
17 |
18 | // Act
19 | using (var writer = new PoWriter(_stream))
20 | {
21 | writer.WriteRecord(localizableString);
22 | }
23 |
24 | // Assert
25 | var result = ReadPoStream();
26 | Assert.Equal($"msgid \"Computer\"", result[0]);
27 | Assert.Equal($"msgstr \"\"", result[1]);
28 | }
29 |
30 | [Fact]
31 | public void WriteRecord_Escapes()
32 | {
33 | // Arrange
34 | var localizableString = new LocalizableString
35 | {
36 | Text = "Computer \r\n"
37 | };
38 |
39 | // Act
40 | using (var writer = new PoWriter(_stream))
41 | {
42 | writer.WriteRecord(localizableString);
43 | }
44 |
45 | // Assert
46 | var result = ReadPoStream();
47 | Assert.Equal($"msgid \"Computer \\r\\n\"", result[0]);
48 | Assert.Equal($"msgstr \"\"", result[1]);
49 | }
50 |
51 | [Fact]
52 | public void WriteRecord_WritesPluralLocalizableString()
53 | {
54 | // Arrange
55 | var localizableString = new LocalizableString()
56 | {
57 | Text = "Computer",
58 | TextPlural = "Computers"
59 | };
60 |
61 | // Act
62 | using (var writer = new PoWriter(_stream))
63 | {
64 | writer.WriteRecord(localizableString);
65 | }
66 |
67 | // Assert
68 | var result = ReadPoStream();
69 | Assert.Equal($"msgid \"Computer\"", result[0]);
70 | Assert.Equal($"msgid_plural \"Computers\"", result[1]);
71 | Assert.Equal($"msgstr[0] \"\"", result[2]);
72 | }
73 |
74 | [Fact]
75 | public void WriteRecord_WritesContext()
76 | {
77 | // Arrange
78 | var localizableString = new LocalizableString()
79 | {
80 | Text = "Computer",
81 | Context = "CONTEXT"
82 | };
83 |
84 | // Act
85 | using (var writer = new PoWriter(_stream))
86 | {
87 | writer.WriteRecord(localizableString);
88 | }
89 |
90 | // Assert
91 | var result = ReadPoStream();
92 | Assert.Equal($"msgctxt \"CONTEXT\"", result[0]);
93 | Assert.Equal($"msgid \"Computer\"", result[1]);
94 | Assert.Equal($"msgstr \"\"", result[2]);
95 | }
96 |
97 | [Fact]
98 | public void WriteRecord_WritesLocations()
99 | {
100 | // Arrange
101 | var localizableString = new LocalizableString()
102 | {
103 | Text = "Computer",
104 | };
105 |
106 | localizableString.Locations.Add(new LocalizableStringLocation
107 | {
108 | SourceFile = "File.cs",
109 | SourceFileLine = 1,
110 | Comment = "Comment 1"
111 | });
112 |
113 | localizableString.Locations.Add(new LocalizableStringLocation
114 | {
115 | SourceFile = "File.cs",
116 | SourceFileLine = 2,
117 | Comment = "Comment 2"
118 | });
119 |
120 | // Act
121 | using (var writer = new PoWriter(_stream))
122 | {
123 | writer.WriteRecord(localizableString);
124 | }
125 |
126 | // Assert
127 | var result = ReadPoStream();
128 | Assert.Equal($"#: File.cs:1", result[0]);
129 | Assert.Equal($"#. Comment 1", result[1]);
130 | Assert.Equal($"#: File.cs:2", result[2]);
131 | Assert.Equal($"#. Comment 2", result[3]);
132 | Assert.Equal($"msgid \"Computer\"", result[4]);
133 | Assert.Equal($"msgstr \"\"", result[5]);
134 | }
135 |
136 | private string[] ReadPoStream()
137 | {
138 | using var reader = new StreamReader(new MemoryStream(_stream.ToArray()));
139 | return reader
140 | .ReadToEnd()
141 | .Split(Environment.NewLine);
142 | }
143 | }
144 |
--------------------------------------------------------------------------------