Skip to content

Commit eb5a5d6

Browse files
authored
[NEW-FEATURE] Implement use of resx for translation of PLC strings (#144)
* Create draft PR for #143 * [ixr] multiple adjustments to allow use of resx in twin projects - [breaking] the ixc now creates csproj files with names that comply with identifier requirements (due to resx generator using the project name by default to generate resources) files with name using '-' will now use instead '_' (e.g. ax-blazor-example.csproj will be ax_blazor_example.csproj). - ixr when no parameter provided will use current directory (apax) as the source (-x) and will retrieve the information about the output project from 'AXSharp.config.json' file. Providing output paramters will not alter the content of config file. - resource identifier creator was moved to AXSharp.Connector to share the same logic when creating and searching indentifiers. - resource injection for the translator [wip] * comiler now adds reference to assembly resources for plc string translation - we create new class `PlcTranslator` for each assembly (twin project) that provides `Resources.PlcStringResources` as default resource for given assembly. All members that can contain data eligible for localizaions refernce the singleton of this 'PlcTranslator'. * added missing expected files for testing to sc * updates docfx to 2.64.0 * namespace removed from usings due to a problem in documentation generation * adds infomation about parameterless execution of ixr * multiple addition due localization - adds translator to generated added properties of string type - simplifies translator - refactoring localization helper to AXSharp.Connector.Localization namespace * adds documenation how-to localization --------- Co-authored-by: PTKu <PTKu@users.noreply.github.com>
1 parent 2f9954f commit eb5a5d6

File tree

121 files changed

+2450
-1535
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+2450
-1535
lines changed

.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
]
1010
},
1111
"docfx": {
12-
"version": "2.63.0",
12+
"version": "2.64.0",
1313
"commands": [
1414
"docfx"
1515
]

build-docs.ps1

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
# run build
2-
dotnet run --project cake/Build.csproj -- $args --do-docs true
3-
exit $LASTEXITCODE;
1+
.\build.ps1
2+
dotnet docfx .\docfx\docfx.json

docfx/articles/connectors/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,48 @@ HumanReadable property is the concatenation of AttributeName properties in the t
309309

310310
HumanReadable from `TurnLightsOff` is `App.Settings.Lights off`
311311

312+
313+
## Controller string localization
314+
315+
### Localizable string
316+
317+
Any string or part of a string that is enclosed between `<#` and `#>` will be marked for translation. This feature will work with types `STRING` and `WSTRING` and with the added attributes of `string` type.
318+
319+
320+
*Example of the added attribute of string type*
321+
~~~
322+
{#ix-set:AttributeName = "<#Integer From PLC#>"}
323+
{#ix-set:AttributeName = "(A1)<#Horizontal cylinder#>"}
324+
~~~
325+
326+
*Examples of STRING and WSTRING variables with*
327+
~~~
328+
VAR
329+
someString : STRING;
330+
someWString : WSTRING;
331+
END_VAR
332+
333+
someString := '<#This woule be localized#> and this would stay as it is';
334+
335+
336+
someWString := "<#This woule be localized#> and this would stay as it is";
337+
~~~
338+
339+
Connectors implement features that allow localizing of the texts (controller defined and added attributes of string type). For the localization to work the twin assembly must be provided with a resource file (*.resx). Resource files can be generated using [ixr tool](~/articles/ixr/IXR.md). You will need to add the resource file to your **Twin project** and set the resource code generation to *public*.
340+
341+
![](../../images/ixd/howto_addresource.gif)
342+
343+
The twin project will use the resource `[twin_project_namespace].Resources.PlcStringResources` type as the default resource for the PLC string translations.
344+
345+
If there is no localization resource defined the twin assembly will not translate the string but will provide the original raw string cleaned from localization tokens.
346+
347+
It is possible to override the default resource using [`SetLocalizationResource`](~/api/AXSharp.Connector.Localizations.Translator.yml) method. The method for a given twin assembly is accessible via `[twin_project_default_namespace].PlcTranslator.Instance`.
348+
349+
Example
350+
~~~C#
351+
ix_plc.PlcTranslator.Instance.SetLocalizationResource(typeof(myproject.ResourcesOverride.OverridePlcStringResources));
352+
~~~
353+
312354
See also
313355

314356
[Dummy Connector](Dummy.md)

docfx/articles/ixr/IXR.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ where:
2222

2323
Localizable string are exported only from string attributes and pragmas of attribute name.
2424

25+
`ixr` can be run without parameters from within an AX project folder. In that case it will use the settings from `ixc` `AXSharp.config.json` and emit the resource file into `.g\Resource\PlcStringResource.resx` file of the ax twin project.
26+
27+
>[!NOTE]
28+
>More about implementing resx file into your project see [here](~/articles/connectors/README.md#controller-string-localization).
29+
2530
## Localizable string
2631

2732
Localizable string must start with `<#` and end with `#>` tags.
262 KB
Loading

src/AXSharp.blazor/tests/sandbox/ComponentsExamples/ComponentsExamples.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<ProjectReference Include="..\..\..\..\AXSharp.connectors\src\AXSharp.Connector\AXSharp.Connector.csproj" />
2020
<ProjectReference Include="..\..\..\src\AXSharp.Presentation.Blazor.Controls\AXSharp.Presentation.Blazor.Controls.csproj" />
2121
<ProjectReference Include="..\..\..\src\AXSharp.Presentation.Blazor\AXSharp.Presentation.Blazor.csproj" />
22-
<ProjectReference Include="..\ax-blazor-example\ix\ax-blazor-example.csproj" />
22+
<ProjectReference Include="..\ax-blazor-example\ix\ax_blazor_example.csproj" />
2323

2424
</ItemGroup>
2525

src/AXSharp.blazor/tests/sandbox/ax-blazor-example/ix/ax-blazor-example.csproj renamed to src/AXSharp.blazor/tests/sandbox/ax-blazor-example/ix/ax_blazor_example.csproj

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
3+
<TargetFramework>net6.0</TargetFramework>
44
<ImplicitUsings>enable</ImplicitUsings>
55
<Nullable>enable</Nullable>
66
</PropertyGroup>
7+
78
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net6.0|AnyCPU'">
8-
<NoWarn>1701;1702;1998</NoWarn>
9+
<NoWarn>1701;1702;1998</NoWarn>
910
</PropertyGroup>
1011
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net6.0|AnyCPU'">
11-
<NoWarn>1701;1702;1998</NoWarn>
12+
<NoWarn>1701;1702;1998</NoWarn>
1213
</PropertyGroup>
1314
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0|AnyCPU'">
14-
<NoWarn>1701;1702;1998</NoWarn>
15+
<NoWarn>1701;1702;1998</NoWarn>
1516
</PropertyGroup>
1617
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0|AnyCPU'">
17-
<NoWarn>1701;1702;1998</NoWarn>
18+
<NoWarn>1701;1702;1998</NoWarn>
1819
</PropertyGroup>
1920

2021
<ItemGroup>
21-
<ProjectReference Include="..\..\..\..\..\AXSharp.abstractions\src\AXSharp.Abstractions\AXSharp.Abstractions.csproj" />
22-
<ProjectReference Include="..\..\..\..\..\AXSharp.connectors\src\AXSharp.Connector\AXSharp.Connector.csproj" />
22+
<ProjectReference Include="..\..\..\..\..\AXSharp.abstractions\src\AXSharp.Abstractions\AXSharp.Abstractions.csproj" />
23+
<ProjectReference Include="..\..\..\..\..\AXSharp.connectors\src\AXSharp.Connector\AXSharp.Connector.csproj" />
2324
</ItemGroup>
2425

2526
<ItemGroup>
2627
<Compile Include=".g\**" />
2728
</ItemGroup>
2829

29-
30+
3031
<ItemGroup>
3132
<Folder Include=".g\" />
3233
</ItemGroup>

src/AXSharp.compiler/src/AXSharp.Compiler.Abstractions/TargetProject/ITargetProject.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ public interface ITargetProject
2323
/// </summary>
2424
void ProvisionProjectStructure();
2525

26+
void GenerateResources();
27+
2628
IEnumerable<IReference> LoadReferences();
2729
}

src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpProject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public void Generate()
130130

131131
TargetProject.ProvisionProjectStructure();
132132
GenerateMetadata(compilation);
133-
133+
TargetProject.GenerateResources();
134134
Log.Logger.Information($"Compilation of project '{AxProject.SrcFolder}' done.");
135135
}
136136

src/AXSharp.compiler/src/AXSharp.Cs.Compiler/CsProject.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public CsProject(AXSharpProject AXSharpProject)
4949
private static string MakeValidFileName(string name)
5050
{
5151
var fileName = name;
52-
foreach (var c in Path.GetInvalidFileNameChars()) fileName = fileName.Replace(c, '_');
52+
//foreach (var c in Path.GetInvalidFileNameChars()) fileName = fileName.Replace(c, '_');
53+
fileName = MakeValidIdentifier(fileName);
5354

5455
fileName = fileName.Replace('@', '_');
5556

@@ -132,6 +133,39 @@ public void ProvisionProjectStructure()
132133
EnsureCsProjFile();
133134
}
134135

136+
public void GenerateResources()
137+
{
138+
var a = @$"
139+
using System.Reflection;
140+
using AXSharp.Connector.Localizations;
141+
142+
namespace {this.ProjectRootNamespace}
143+
{{
144+
public sealed class PlcTranslator : Translator
145+
{{
146+
private static readonly PlcTranslator instance = new PlcTranslator();
147+
148+
public static PlcTranslator Instance
149+
{{
150+
get
151+
{{
152+
return instance;
153+
}}
154+
}}
155+
156+
private PlcTranslator()
157+
{{
158+
var defaultResourceType = Assembly.GetAssembly(typeof({this.ProjectRootNamespace}.PlcTranslator))
159+
.GetType(""{this.ProjectRootNamespace}.Resources.PlcStringResources"");
160+
this.SetLocalizationResource(defaultResourceType);
161+
}}
162+
}}
163+
}}";
164+
165+
using var swr = new StreamWriter(Path.Combine(this.AxSharpProject.OutputFolder, ".g", "PlcResources.g.cs"));
166+
swr.Write(a);
167+
}
168+
135169

136170
/// <inheritdoc />
137171
public string GetMetaDataFolder => Path.Combine(AxSharpProject.OutputFolder, ".meta");

src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Onliner/CsOnlinerSourceBuilder.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// https://github.com/ix-ax/axsharp/blob/dev/LICENSE
66
// Third party licenses: https://github.com/ix-ax/axsharp/blob/master/notices.md
77

8+
using System.Security.Cryptography.X509Certificates;
89
using System.Text;
910
using AX.ST.Semantic;
1011
using AX.ST.Semantic.Model.Declarations;
@@ -49,6 +50,8 @@ public void CreateFile(IFileSyntax fileSyntax, IxNodeVisitor visitor)
4950
AddToSource("using AXSharp.Connector;");
5051
AddToSource("using AXSharp.Connector.ValueTypes;");
5152
AddToSource("using System.Collections.Generic;");
53+
AddToSource("using AXSharp.Connector.Localizations;");
54+
5255
fileSyntax.Declarations.ToList().ForEach(p => p.Visit(visitor, this));
5356
}
5457

@@ -280,10 +283,11 @@ private void CreateITwinObjectImplementation()
280283
$"public {typeof(ITwinObject).n()} GetParent() {{ return this.@Parent; }}" +
281284
"public string Symbol { get; protected set; }" +
282285
"private string _attributeName;" +
283-
"public System.String AttributeName { get {return AXSharp.Localizations.LocalizationHelper.CleanUpLocalizationTokens(_attributeName); } set { _attributeName = value; } }" +
286+
"public System.String AttributeName { get => string.IsNullOrEmpty(_attributeName) ? SymbolTail : this.Translate(_attributeName).Interpolate(this); set => _attributeName = value; }" +
284287
"public string HumanReadable { get; set; }" +
285288
"protected System.String @SymbolTail { get; set;}" +
286-
$"protected {typeof(ITwinObject).n()} @Parent {{ get; set; }}"
289+
$"protected {typeof(ITwinObject).n()} @Parent {{ get; set; }}"+
290+
$"public AXSharp.Connector.Localizations.Translator Interpreter => {Project.TargetProject.ProjectRootNamespace}.PlcTranslator.Instance;"
287291
);
288292
}
289293

src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Pragmas/PragmaParser/Ast/AddedPropertyDeclarationAstNode.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,12 @@ public override void AcceptVisitor(IAstVisitor visitor)
2828
{
2929
if (visitor is PragmaVisitor v)
3030
{
31-
if (Type.ToUpperInvariant() == "STRING")
31+
if (Type.ToUpperInvariant() == "STRING" || Type.ToUpperInvariant() == "WSTRING")
3232
{
3333
v.Product = $"private {Type} _{Identifier};" +
3434
$"\n{AccessQualifier} {Type} {Identifier} " +
3535
$"{{ " +
36-
$"get" +
37-
$"{{ " +
38-
$"return AXSharp.Localizations.LocalizationHelper.CleanUpLocalizationTokens(_{Identifier}); " +
39-
$"}} " +
40-
$"set " +
41-
$"{{_{Identifier} = value;" +
42-
$"}} " +
36+
$"get => string.IsNullOrEmpty(_{Identifier}) ? SymbolTail : this.Translate(_{Identifier}).Interpolate(this); set => _{Identifier} = value; " +
4337
$"}}";
4438
}
4539
else

src/AXSharp.compiler/src/ixc/Properties/launchSettings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
"ixc": {
44
"commandName": "Project",
55
"workingDirectory": "c:\\W\\Develop\\gh\\ix-ax\\statemachine\\"
6+
},
7+
"integration_plc": {
8+
"commandName": "Project",
9+
"workingDirectory": "c:\\W\\Develop\\gh\\ix-ax\\axsharp\\src\\sanbox\\integration\\ix-integration-plc\\"
610
}
711
}
812
}

src/AXSharp.compiler/src/ixr/AXSharp.ixr.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
</ItemGroup>
9797

9898
<ItemGroup>
99+
<ProjectReference Include="..\..\..\AXSharp.connectors\src\AXSharp.Connector\AXSharp.Connector.csproj" />
99100
<ProjectReference Include="..\AXSharp.Compiler\AXSharp.Compiler.csproj" />
100101
</ItemGroup>
101102

src/AXSharp.compiler/src/ixr/LocalizedStringWrapper.cs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,6 @@ public LocalizedStringWrapper()
2323
RegexOptions.Compiled | RegexOptions.IgnoreCase);
2424

2525

26-
}
27-
28-
public string CreateId(string rawText)
29-
{
30-
StringBuilder sb = new StringBuilder();
31-
foreach (var c in rawText)
32-
{
33-
//try to find character in mappings
34-
var mapchar = SymbolMappings.TryToGetValueByChar(c);
35-
if(mapchar != null)
36-
{
37-
sb.Append(mapchar);
38-
}
39-
//add only if is letter
40-
else if (Char.IsLetter(c))
41-
{
42-
sb.Append(c);
43-
}
44-
else
45-
{
46-
//unknown
47-
sb.Append('_');
48-
}
49-
}
50-
return sb.ToString();
51-
52-
5326
}
5427

5528
public bool IsValidId(string id)

src/AXSharp.compiler/src/ixr/Options.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ namespace AXSharp.ixc_doc
1212
{
1313
internal class Options : ICompilerOptions
1414
{
15-
[Option('x', "source-project-folder", Required = true, HelpText = "Simatic-ax project folder")]
15+
[Option('x', "source-project-folder", Required = false, HelpText = "Simatic-ax project folder")]
1616
public string? AxSourceProjectFolder { get; set; }
1717

18-
[Option('o', "output-project-folder", Required = true, HelpText = "Output project folder where compiler emits result.")]
18+
[Option('o', "output-project-folder", Required = false, HelpText = "Output project folder where compiler emits result.")]
1919
public string? OutputProjectFolder { get; set; }
2020

2121
}

src/AXSharp.compiler/src/ixr/Program.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@
2020
using static System.Net.Mime.MediaTypeNames;
2121
using System.Text.RegularExpressions;
2222

23+
2324
const string Logo =
24-
@"| \ /
25-
= =
26-
| / \
25+
@" ___ ___ ___ _ _
26+
/ \ \ \ / / _| || |_
27+
/ ^ \ \ V / |_ __ _|
28+
/ /_\ \ > < _| || |_
29+
/ _____ \ / . \ |_ __ _|
30+
/__/ \__\ /__/ \__\ |_||_|
2731
";
2832

2933

3034
Console.WriteLine(Logo);
3135
LegalAcrobatics.LegalComplianceAcrobatics(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName).Wait();
32-
Console.WriteLine("Ixr compiler - compile plc localized strings to resx dictonaries");
36+
Console.WriteLine("Ixr compiler - compiles plc localized strings resources to resx");
37+
Console.WriteLine($"Version: {GitVersionInformation.SemVer}");
3338
Parser.Default.ParseArguments<Options>(args)
3439
.WithParsed(o =>
3540
{
@@ -54,8 +59,20 @@
5459

5560
void Generate(Options o)
5661
{
57-
var axProject = new AxProject(o.AxSourceProjectFolder);
62+
var axProjectFolder = string.IsNullOrEmpty(o.AxSourceProjectFolder)
63+
? Environment.CurrentDirectory
64+
: o.AxSourceProjectFolder;
65+
66+
var axProject = new AxProject(axProjectFolder);
67+
var axProjectConfig =
68+
AXSharpConfig.RetrieveIxConfig(Path.Combine(axProject.ProjectFolder, AXSharpConfig.CONFIG_FILE_NAME));
69+
70+
(string folder, string file) output = string.IsNullOrEmpty(axProjectConfig.OutputProjectFolder)
71+
? (string.Empty, o.OutputProjectFolder)
72+
: (Path.GetFullPath(Path.Combine(axProject.ProjectFolder, axProjectConfig.OutputProjectFolder, "Resources")), "PlcStringResources.resx");
73+
5874
Console.WriteLine($"Compiling project {axProject.ProjectInfo.Name}...");
75+
5976
var projectSources = axProject.Sources.Select(p => (parseTree: STParser.ParseTextAsync(p).Result, source: p));
6077

6178
var syntaxTrees = projectSources.Select(p => p.parseTree);
@@ -65,13 +82,13 @@ void Generate(Options o)
6582
//iterate all syntax trees from project
6683
foreach (var syntaxTree in syntaxTrees)
6784
{
68-
IterateSyntaxTreeForStringLiterals(syntaxTree.GetRoot(),lw, Path.GetRelativePath(o.AxSourceProjectFolder, syntaxTree.Filename));
85+
IterateSyntaxTreeForStringLiterals(syntaxTree.GetRoot(),lw, Path.GetRelativePath(axProjectFolder, syntaxTree.Filename));
6986

70-
IterateSyntaxTreeForPragmas(syntaxTree.GetRoot(), lw, Path.GetRelativePath(o.AxSourceProjectFolder, syntaxTree.Filename));
87+
IterateSyntaxTreeForPragmas(syntaxTree.GetRoot(), lw, Path.GetRelativePath(axProjectFolder, syntaxTree.Filename));
7188
}
7289

7390
//add resources from dictionary to resx file
74-
ResxManager.AddResourcesFromDictionary(o.OutputProjectFolder, lw.LocalizedStringsDictionary);
91+
ResxManager.AddResourcesFromDictionary(output.folder, output.file, lw.LocalizedStringsDictionary);
7592
}
7693

7794
void IterateSyntaxTreeForStringLiterals(ISyntaxNode root, LocalizedStringWrapper lw, string fileName)
@@ -115,7 +132,7 @@ void AddToDictionaryIfLocalizedString(ISyntaxToken token, LocalizedStringWrapper
115132
var rawText = lw.GetRawTextFromLocalizedString(localizedString);
116133

117134
//create id
118-
var id = lw.CreateId(rawText);
135+
var id = AXSharp.Connector.Localizations.LocalizationHelper.CreateId(rawText);
119136

120137
//check if identifier is valid
121138
if(lw.IsValidId(id))

0 commit comments

Comments
 (0)