diff --git a/ClaudeDo.slnx b/ClaudeDo.slnx index ad80295..efcd91e 100644 --- a/ClaudeDo.slnx +++ b/ClaudeDo.slnx @@ -6,6 +6,7 @@ + @@ -13,5 +14,6 @@ + diff --git a/src/ClaudeDo.Localization/ClaudeDo.Localization.csproj b/src/ClaudeDo.Localization/ClaudeDo.Localization.csproj new file mode 100644 index 0000000..3b41c04 --- /dev/null +++ b/src/ClaudeDo.Localization/ClaudeDo.Localization.csproj @@ -0,0 +1,10 @@ + + + net8.0 + enable + enable + + + + + diff --git a/src/ClaudeDo.Localization/LocaleFile.cs b/src/ClaudeDo.Localization/LocaleFile.cs new file mode 100644 index 0000000..18ca7c0 --- /dev/null +++ b/src/ClaudeDo.Localization/LocaleFile.cs @@ -0,0 +1,15 @@ +namespace ClaudeDo.Localization; + +public sealed class LocaleFile +{ + public LocaleFile(string code, string name, IReadOnlyDictionary strings) + { + Code = code; + Name = name; + Strings = strings; + } + + public string Code { get; } + public string Name { get; } + public IReadOnlyDictionary Strings { get; } +} diff --git a/src/ClaudeDo.Localization/LocaleJson.cs b/src/ClaudeDo.Localization/LocaleJson.cs new file mode 100644 index 0000000..0a3802e --- /dev/null +++ b/src/ClaudeDo.Localization/LocaleJson.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +namespace ClaudeDo.Localization; + +public static class LocaleJson +{ + public static LocaleFile Parse(string json) + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + var code = ""; + var name = ""; + if (root.TryGetProperty("metadata", out var meta) && meta.ValueKind == JsonValueKind.Object) + { + if (meta.TryGetProperty("code", out var c)) code = c.GetString() ?? ""; + if (meta.TryGetProperty("name", out var n)) name = n.GetString() ?? ""; + } + + var strings = new Dictionary(StringComparer.Ordinal); + foreach (var prop in root.EnumerateObject()) + { + if (prop.NameEquals("metadata")) continue; + Flatten(prop.Name, prop.Value, strings); + } + + return new LocaleFile(code, name, strings); + } + + private static void Flatten(string prefix, JsonElement el, IDictionary into) + { + switch (el.ValueKind) + { + case JsonValueKind.Object: + foreach (var p in el.EnumerateObject()) + Flatten($"{prefix}.{p.Name}", p.Value, into); + break; + case JsonValueKind.String: + into[prefix] = el.GetString() ?? ""; + break; + default: + into[prefix] = el.ToString(); + break; + } + } +} diff --git a/tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj b/tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj new file mode 100644 index 0000000..66838f4 --- /dev/null +++ b/tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj @@ -0,0 +1,20 @@ + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + diff --git a/tests/ClaudeDo.Localization.Tests/LocaleJsonTests.cs b/tests/ClaudeDo.Localization.Tests/LocaleJsonTests.cs new file mode 100644 index 0000000..d906948 --- /dev/null +++ b/tests/ClaudeDo.Localization.Tests/LocaleJsonTests.cs @@ -0,0 +1,38 @@ +using ClaudeDo.Localization; + +namespace ClaudeDo.Localization.Tests; + +public class LocaleJsonTests +{ + private const string Sample = """ + { + "metadata": { "code": "en", "name": "English" }, + "settings": { "save": "Save", "general": { "model": "Model" } }, + "tasks": { "addPlaceholder": "Add a task" } + } + """; + + [Fact] + public void Parse_reads_metadata() + { + var f = LocaleJson.Parse(Sample); + Assert.Equal("en", f.Code); + Assert.Equal("English", f.Name); + } + + [Fact] + public void Parse_flattens_nested_keys_to_dot_paths() + { + var f = LocaleJson.Parse(Sample); + Assert.Equal("Save", f.Strings["settings.save"]); + Assert.Equal("Model", f.Strings["settings.general.model"]); + Assert.Equal("Add a task", f.Strings["tasks.addPlaceholder"]); + } + + [Fact] + public void Parse_excludes_metadata_from_strings() + { + var f = LocaleJson.Parse(Sample); + Assert.False(f.Strings.ContainsKey("metadata.code")); + } +}