diff --git a/src/wan24-I8NKws/KwsCatalog.cs b/src/wan24-I8NKws/KwsCatalog.cs
new file mode 100644
index 0000000..de92c01
--- /dev/null
+++ b/src/wan24-I8NKws/KwsCatalog.cs
@@ -0,0 +1,88 @@
+using System.ComponentModel.DataAnnotations;
+using wan24.Core;
+using wan24.ObjectValidation;
+
+namespace wan24.I8NKws
+{
+ ///
+ /// KWS catalog
+ ///
+ public sealed class KwsCatalog
+ {
+ ///
+ /// Constructor
+ ///
+ public KwsCatalog() { }
+
+ ///
+ /// Project name
+ ///
+ public string Project { get; set; } = string.Empty;
+
+ ///
+ /// Created time (UTC)
+ ///
+ public DateTime Created { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Modified time (UTC)
+ ///
+ public DateTime Modified { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Translator name
+ ///
+ public string Translator { get; set; } = string.Empty;
+
+ ///
+ /// Locale identifier
+ ///
+ [RegularExpression(RegularExpressions.LOCALE_WITH_DASH)]
+ public string Locale { get; set; } = "en-US";
+
+ ///
+ /// If the text is written right to left
+ ///
+ public bool RightToLeft { get; set; }
+
+ ///
+ /// Keywords
+ ///
+ public HashSet Keywords { get; } = [];
+
+ ///
+ /// Validate the catalog
+ ///
+ /// Throw an exception on error?
+ /// Require all translations to be complete?
+ /// If the catalog is valid
+ /// Catalog is invalid
+ public bool Validate(in bool throwOnError = true, in bool requireCompleteTranslations = false)
+ {
+ if (Keywords.Count == 0)
+ {
+ if (!throwOnError) return false;
+ throw new InvalidDataException("Missing keywords");
+ }
+ foreach (KwsKeyword keyword in Keywords)
+ {
+ if (string.IsNullOrEmpty(keyword.ID))
+ {
+ if (!throwOnError) return false;
+ throw new InvalidDataException("Found keyword with missing ID");
+ }
+ if (requireCompleteTranslations && keyword.TranslationMissing)
+ {
+ if (!throwOnError) return false;
+ throw new InvalidDataException($"Missing translation of ID \"{keyword.ID}\"");
+ }
+ }
+ if(!this.TryValidateObject(out List results, throwOnError: false))
+ {
+ if (!throwOnError) return false;
+ throw new InvalidDataException($"Found {results.Count} object errors - first error: {results.First().ErrorMessage}");
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/wan24-I8NKws/KwsKeyword.cs b/src/wan24-I8NKws/KwsKeyword.cs
new file mode 100644
index 0000000..bc3fbbb
--- /dev/null
+++ b/src/wan24-I8NKws/KwsKeyword.cs
@@ -0,0 +1,122 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+namespace wan24.I8NKws
+{
+ ///
+ /// KWS keyword record
+ ///
+ public sealed record class KwsKeyword
+ {
+ ///
+ /// Constructor
+ ///
+ public KwsKeyword() { }
+
+ ///
+ /// Constructor
+ ///
+ /// ID
+ public KwsKeyword(in string id)
+ {
+ if (id.Length < 1) throw new ArgumentException("ID required", nameof(id));
+ ID = id;
+ }
+
+ ///
+ /// ID (keyword)
+ ///
+ [MinLength(1)]
+ public string ID { get; private set; } = null!;
+
+ ///
+ /// Previous IDs (extended when the ID is being updated; last entry was the latest ID)
+ ///
+ public HashSet PreviousIds { get; private set; } = [];
+
+ ///
+ /// Extracted time (UTC)
+ ///
+ public DateTime Extracted { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Updated time (UTC)
+ ///
+ public DateTime Updated { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Latest translator name
+ ///
+ public string Translator { get; set; } = string.Empty;
+
+ ///
+ /// Developer comments
+ ///
+ public string DeveloperComments { get; set; } = string.Empty;
+
+ ///
+ /// Translator comments
+ ///
+ public string TranslatorComments { get; set; } = string.Empty;
+
+ ///
+ /// If this keyword is obsolete and should not be exported
+ ///
+ public bool Obsolete { get; set; }
+
+ ///
+ /// If the update of the ID has been done automatic using fuzzy logic search
+ ///
+ public bool Fuzzy { get; set; }
+
+ ///
+ /// If a translation is missing (no translation at all, or any empty translations)
+ ///
+ [JsonIgnore]
+ public bool TranslationMissing => Translations.Count == 0 || Translations.Any(t => t.Length == 0);
+
+ ///
+ /// Translations
+ ///
+ public List Translations { get; } = [];
+
+ ///
+ /// Source references
+ ///
+ public HashSet SourceReferences { get; } = [];
+
+ ///
+ /// Update the ID
+ ///
+ /// New ID
+ public void UpdateId(in string newId)
+ {
+ if (Obsolete) throw new InvalidOperationException();
+ if (newId.Length < 1) throw new ArgumentException("ID required", nameof(newId));
+ string oldId = ID;
+ if (newId == oldId) return;
+ ID = newId;
+ PreviousIds.Remove(oldId);
+ PreviousIds.Add(oldId);
+ }
+
+ ///
+ /// Undo an ID update
+ ///
+ /// Target ID to use
+ public void UndoIdUpdate(string? id = null)
+ {
+ if (Obsolete || PreviousIds.Count == 0) throw new InvalidOperationException();
+ if (id is null)
+ {
+ id = PreviousIds.Last();
+ }
+ else if (!PreviousIds.Contains(id))
+ {
+ throw new ArgumentException("Unknown previous ID", nameof(id));
+ }
+ ID = id;
+ PreviousIds = [.. PreviousIds.SkipWhile(pid => pid != id).Skip(1)];
+ }
+ }
+}
diff --git a/src/wan24-I8NKws/KwsSourceReference.cs b/src/wan24-I8NKws/KwsSourceReference.cs
new file mode 100644
index 0000000..e0c04c7
--- /dev/null
+++ b/src/wan24-I8NKws/KwsSourceReference.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace wan24.I8NKws
+{
+ ///
+ /// KWS source reference
+ ///
+ public sealed record class KwsSourceReference
+ {
+ ///
+ /// Constructor
+ ///
+ public KwsSourceReference() { }
+
+ ///
+ /// Filename
+ ///
+ [MinLength(1)]
+ public required string FileName { get; init; }
+
+ ///
+ /// Line number (starts with 1)
+ ///
+ [Range(1, int.MaxValue)]
+ public required int LineNumber { get; init; }
+ }
+}
diff --git a/src/wan24-I8NKws/LICENSE b/src/wan24-I8NKws/LICENSE
new file mode 100644
index 0000000..486ce20
--- /dev/null
+++ b/src/wan24-I8NKws/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Andreas Zimmermann, wan24.de
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/wan24-I8NKws/README.md b/src/wan24-I8NKws/README.md
new file mode 100644
index 0000000..9bb3c27
--- /dev/null
+++ b/src/wan24-I8NKws/README.md
@@ -0,0 +1,228 @@
+# wan24-I8NTool
+
+This is a small dotnet tool for extracting stings to translate from source
+code and writing the result in the PO format to a file or STDOUT. It can also
+be used to create i8n files, which are easy to use from any app.
+
+It's pre-configured for use with the
+[`wan24-Core`](https://github.com/WAN-Solutions/wan24-Core) translation
+helpers for C#, but it can be customized easily for any environment and any
+programming language by customizing the used regular expressions in your own
+configuration file (find available presets in the `config` folder of this
+repository).
+
+## Usage
+
+### Where to get it
+
+This is a dotnet tool and can be installed from the command line:
+
+```bash
+dotnet tool install -g wan24-I8NTool
+```
+
+The default installation folder is
+
+- `%USER%\.dotnet\tools` for Windows
+- `~/.dotnet/tools` for Linux (or MAC)
+
+**NOTE**: Please ensure that your global .NET tool path is in the `PATH`
+environment variable (open a new Windows terminal after adding the path using
+_Settings_ -> _System_ -> _Extended system settings_ -> _Extended_ ->
+_Environment variables_).
+
+### Simple usage
+
+With pre-configuration for .NET:
+
+```bash
+wan24I8NTool --input ./ > keywords.po
+```
+
+You can find other pre-configurations in the `config/*.json` files in the
+GitHub repository:
+
+```bash
+wan24I8NTool --config file.json --input ./ > keywords.po
+```
+
+### Default keyword extraction process
+
+Per default keywords will be found by the regular expressions you can find in
+`config/dotnet.json` (without replacement). They'll then be post-processed by
+the replacement expressions, until the final quoted keyword was extracted from
+a line.
+
+To force including any string (from a constant definition, for example),
+simply add a comment `// wan24I8NTool:include` at the end of the line -
+example:
+
+```cs
+public const string NAME = "Any PO included keyword";// wan24I8NTool:include
+```
+
+**NOTE**: (Multiline) concatenated string value definitions (like
+`"Part a" + "Part b"`) or interpolations can't be parsed. The matched keyword
+must be C style escaped.
+
+### Custom parser configuration
+
+In the `config/dotnet.json` file of this repository you find the default
+configuration. You can download and modify it for your needs, and use it with
+the `--config` parameter.
+
+The configuration allows to define regular expressions, where
+
+- an array with two elements is a regular expression (and its `RegexOptions`
+enumeration value) which needs to match the string to use
+- an array with three elements is used to replace a pattern (the 3rd element
+is the replacement), if the regular expression does match
+
+Example parser JSON configuration:
+
+```json
+{
+ "SingleThread": false,// (optional) Set to true to disable multithreading (may be overridden by -singleThread)
+ "Encoding": "UTF-8",// (optional) Source encoding to use (default is UTF-8; may be overridden by --encoding)
+ "Patterns": [// (optional)
+ ["Any regular expression", "None"],// Search expression example
+ ["Any regular search expression", "None", "Replacement"],// Replacement expression example
+ ...
+ ],
+ "FileExtensions": [// (optional) File extensions to include when walking through a folder tree (may be overridden by --ext)
+ ".ext",
+ ...
+ ],
+ "MergeOutput": true,// (optional) Merge the extracted keywords to the existing output PO file
+ "FailOnError": true,// (optional) To fail thewhole process on any error
+ "Merge": false// (optional) Set to true to merge your custom configuration with the default configuration
+}
+```
+
+The parser looks for any matching search-only expression, then applies all
+matching replacement expressions to refer to the keyword to use, finally. If
+no replacement matched the search expression string, the full search match
+will be the used keyword.
+
+During merging, lists will be combined, and single options will be overwritten.
+
+There are some more optional keys for advanced configuration:
+
+- `Core`: [`wan24-Core`](https://github.com/WAN-Solutions/wan24-Core)
+configuration using a `AppConfig` structure
+- `CLI`: [`wan24-CLI`](https://github.com/nd1012/wan24-CLI) configuration
+using a `CliAppConfig` structure
+
+### Build, extract, display and use an i8n file
+
+i8n files contain optional compressed translation terms. They can be created
+from PO/MO and/or JSON dictionary (keyword as key, translation as an array of
+strings as value) input files like this:
+
+```bash
+wan24I8NTool i8n -compress --poInput /path/to/input.po --output /path/to/output.i8n
+```
+
+An i8n file can be embedded into an app, for example.
+
+To convert all `*.json|po|mo` files in the current folder to `*.i8n` files:
+
+```bash
+wan24I8NTool i8n buildmany -compress -verbose
+```
+
+To display some i8n file informations:
+
+```bash
+wan24I8NTool i8n display --input /path/to/input.i8n
+```
+
+To extract some i8n file to a JSON file (prettified):
+
+```bash
+wan24I8NTool i8n extract --input /path/to/input.i8n --jsonOutput /path/to/output.json
+```
+
+To extract some i8n file to a PO file:
+
+```bash
+wan24I8NTool i8n extract --input /path/to/input.i8n --poOutput /path/to/output.po
+```
+
+**NOTE**: For more options and usage instructions please use the CLI API help
+(see below).
+
+#TODO Add wan24-I8N usage instructions
+
+**TIPP**: You can use the i8n API for converting, merging and validating the
+supported source formats also.
+
+#### i8n file structure in detail
+
+If you didn't skip writing a header during build, the first byte contains the
+version number and a flag (bit 8), if the body is compressed. The file body is
+a JSON encoded dictionary, having the keyword as ID, and the translations as
+value (an array of strings with none, one or multiple (plural) translations).
+
+If compressed, the `wan24-Compression` default compression algorithm was used.
+This is Brotli at the time of writing. But please note that
+`wan24-Compression` writes a non-standard header before the body, which is
+required for compatibility of newer `wan24-Compression` library versions with
+older compressed contents.
+
+**NOTE**: For using compressed i8n files, you'll have to use the
+[`wan24-Compression`](https://www.nuget.org/packages/wan24-Compression) NuGet
+package in your .NET app for decompressing the body.
+
+Please see the `I8NApi(.Internals).cs` source code in this GitHub repository
+for C# code examples.
+
+**TIPP**: Use compression and the i8n header only, if you're using the i8n
+file from a .NET app. Without a header and compression you can simply
+deserialize the JSON dictionary from the i8n file using any modern programming
+language.
+
+### Manual usage from the command line
+
+If you want to call the dotnet tool manually and use advanced options, you can
+display help like this:
+
+```bash
+wan24I8NTool help (--api API (--method METHOD)) (-details)
+```
+
+For individual usage support, please
+[open an issue here](https://github.com/nd1012/wan24-I8NTool/issues).
+
+**NOTE**: The `wan4-Core` CLI configuration (`CliConfig`) will be applied, so
+advanced configuration is possible using those special command line arguments.
+
+### Steps to i8n
+
+Internationalization (i8n) for apps is a common task to make string used in
+apps translatable. gettext is a tool which have been around for many years now
+and seem to satisfy developers, translators and end users.
+
+The steps to i8n your app are:
+
+1. use i8n methods in your code when you want to translate a term
+1. extract keywords (terms) from your source code into a PO file using an
+extractor
+1. translate the terms using an editor tool and create a MO file
+1. load the MO file using your apps gettext-supporting library
+
+`wan24-I8NTool` is a CLI tool which you can use as extractor to
+automatize things a bit more.
+
+If you'd like to use the i8n file format from `wan24-I8NTool` in your
+.NET app, the last step is replaced by:
+
+- convert the PO/MO file to an i8n file using `wan24-I8NTool`
+- load the i8n file using your .NET app using the `wan24-I8N` library
+
+This is one additional step, but maybe worth it, if you don't want to miss
+features like compressed i8n files ready-to-use i8n `wan24-Core` localization
+(l10n) features. You'll also not need to reference any gettext supporting
+library or do the parsing of the PO/MO format by yourself.
+
+#TODO Links to Github
diff --git a/src/wan24-I8NKws/wan24-I8NKws.csproj b/src/wan24-I8NKws/wan24-I8NKws.csproj
new file mode 100644
index 0000000..fba3d85
--- /dev/null
+++ b/src/wan24-I8NKws/wan24-I8NKws.csproj
@@ -0,0 +1,53 @@
+
+
+
+ net8.0
+ wan24.I8NKws
+ enable
+ enable
+ wan24I8NKws
+ True
+ Debug;Release;Trunk
+ wan24-I8NKws
+ wan24-I8NKws
+ 1.0.0
+ nd1012
+ Andreas Zimmermann, wan24.de
+ wan24-I8NKws
+ Keyword catalog for translations
+ (c)2024 Andreas Zimmermann, wan24.de
+ https://github.com/nd1012/wan24-I8NTool
+ README.md
+ https://github.com/nd1012/wan24-I8NTool
+ git
+ i8n
+ LICENSE
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \
+ True
+
+
+ \
+ True
+
+
+
+
diff --git a/src/wan24-I8NTool CLI/wan24-I8NTool CLI.csproj b/src/wan24-I8NTool CLI/wan24-I8NTool CLI.csproj
index 39d060a..eeac390 100644
--- a/src/wan24-I8NTool CLI/wan24-I8NTool CLI.csproj
+++ b/src/wan24-I8NTool CLI/wan24-I8NTool CLI.csproj
@@ -14,7 +14,7 @@
nd1012
Andreas Zimmermann, wan24.de
wan24-I8NTool
- Poedit source code extractor
+ Source code keyword extractor for i8n
(c)2024 Andreas Zimmermann, wan24.de
https://github.com/nd1012/wan24-I8NTool
README.md
@@ -51,6 +51,7 @@
+
diff --git a/src/wan24-I8NTool.sln b/src/wan24-I8NTool.sln
index d9a70df..284f44d 100644
--- a/src/wan24-I8NTool.sln
+++ b/src/wan24-I8NTool.sln
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stream-Serializer-Extension
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "wan24-Compression", "..\..\wan24-Compression\src\wan24-Compression\wan24-Compression.csproj", "{6378E572-5858-4766-9D26-ADB0DF766BCC}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "wan24-I8NKws", "wan24-I8NKws\wan24-I8NKws.csproj", "{646B706C-F4AD-4007-9873-9628DCB9DE84}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,6 +50,12 @@ Global
{6378E572-5858-4766-9D26-ADB0DF766BCC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6378E572-5858-4766-9D26-ADB0DF766BCC}.Trunk|Any CPU.ActiveCfg = Trunk|Any CPU
{6378E572-5858-4766-9D26-ADB0DF766BCC}.Trunk|Any CPU.Build.0 = Trunk|Any CPU
+ {646B706C-F4AD-4007-9873-9628DCB9DE84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {646B706C-F4AD-4007-9873-9628DCB9DE84}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {646B706C-F4AD-4007-9873-9628DCB9DE84}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {646B706C-F4AD-4007-9873-9628DCB9DE84}.Release|Any CPU.Build.0 = Release|Any CPU
+ {646B706C-F4AD-4007-9873-9628DCB9DE84}.Trunk|Any CPU.ActiveCfg = Trunk|Any CPU
+ {646B706C-F4AD-4007-9873-9628DCB9DE84}.Trunk|Any CPU.Build.0 = Trunk|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE