diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0c14b9f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,265 @@
+root = true
+# 如果要从更高级别的目录继承 .editorconfig 设置,请删除以下行
+
+[*]
+charset = utf-8
+end_of_line = crlf
+insert_final_newline = true
+
+# CA2208: Instantiate argument exceptions correctly
+dotnet_diagnostic.ca2208.severity = none
+dotnet_diagnostic.cs8305.severity = none
+
+# Microsoft .NET properties
+csharp_space_after_cast = true
+
+[*.cs]
+
+#### Core EditorConfig 选项 ####
+
+# 缩进和间距
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+#### .NET 编码约定 ####
+
+# 组织 Using
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = true
+file_header_template =
+
+# this. 和 Me. 首选项
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+# 语言关键字与 BCL 类型首选项
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# 括号首选项
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# 修饰符首选项
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# 表达式级首选项
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# 字段首选项
+dotnet_style_readonly_field = true
+
+# 参数首选项
+dotnet_code_quality_unused_parameters = all
+
+#### C# 编码约定 ####
+
+# var 首选项
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+
+# Expression-bodied 成员
+csharp_style_expression_bodied_accessors = true:suggestion
+# csharp_style_expression_bodied_constructors = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = true:suggestion
+# csharp_style_expression_bodied_methods = true:suggestion
+# IDE0022: 使用表达式主体来表示方法
+csharp_style_expression_bodied_methods = when_on_single_line
+csharp_style_expression_bodied_operators = true:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+
+# 模式匹配首选项
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:suggestion
+csharp_style_prefer_switch_expression = true:suggestion
+
+# null 检查首选项
+csharp_style_conditional_delegate_call = true:suggestion
+
+# 修饰符首选项
+csharp_prefer_static_local_function = true:suggestion
+csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
+
+# 代码块首选项
+csharp_prefer_braces = false:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+# 表达式级首选项
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+
+# using 指令首选项
+csharp_using_directive_placement = outside_namespace:suggestion
+
+#### C# 格式规则 ####
+
+# 新行首选项
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# 缩进首选项
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = false
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# 空格键首选项
+csharp_space_after_cast = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# 包装首选项
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### 命名样式 ####
+
+# 命名规则
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.visible_field_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.visible_field_should_be_pascal_case.symbols = visible_field
+dotnet_naming_rule.visible_field_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.const_field_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.const_field_should_be_pascal_case.symbols = const_field
+dotnet_naming_rule.const_field_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.private_static_field_should_be_pascal_begin_with__.severity = suggestion
+dotnet_naming_rule.private_static_field_should_be_pascal_begin_with__.symbols = private_static_field
+dotnet_naming_rule.private_static_field_should_be_pascal_begin_with__.style = pascal_begin_with__
+
+dotnet_naming_rule.private_static_readonly_field_should_be_pascal_begin_with__.severity = suggestion
+dotnet_naming_rule.private_static_readonly_field_should_be_pascal_begin_with__.symbols = private_static_readonly_field
+dotnet_naming_rule.private_static_readonly_field_should_be_pascal_begin_with__.style = pascal_begin_with__
+
+dotnet_naming_rule.private_field_should_be_camel_begin_with__.severity = suggestion
+dotnet_naming_rule.private_field_should_be_camel_begin_with__.symbols = private_field
+dotnet_naming_rule.private_field_should_be_camel_begin_with__.style = camel_begin_with__
+
+# 符号规范
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.const_field.applicable_kinds = field
+dotnet_naming_symbols.const_field.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.const_field.required_modifiers = const
+
+dotnet_naming_symbols.visible_field.applicable_kinds = field
+dotnet_naming_symbols.visible_field.applicable_accessibilities = public, internal, protected_internal
+dotnet_naming_symbols.visible_field.required_modifiers =
+
+dotnet_naming_symbols.private_field.applicable_kinds = field
+dotnet_naming_symbols.private_field.applicable_accessibilities = private
+dotnet_naming_symbols.private_field.required_modifiers =
+
+dotnet_naming_symbols.private_static_field.applicable_kinds = field
+dotnet_naming_symbols.private_static_field.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_field.required_modifiers = static
+
+dotnet_naming_symbols.private_static_readonly_field.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_field.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly_field.required_modifiers = readonly, static
+
+# 命名样式
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.camel_begin_with__.required_prefix = _
+dotnet_naming_style.camel_begin_with__.required_suffix =
+dotnet_naming_style.camel_begin_with__.word_separator =
+dotnet_naming_style.camel_begin_with__.capitalization = camel_case
+
+dotnet_naming_style.pascal_begin_with__.required_prefix = _
+dotnet_naming_style.pascal_begin_with__.required_suffix =
+dotnet_naming_style.pascal_begin_with__.word_separator =
+dotnet_naming_style.pascal_begin_with__.capitalization = pascal_case
+
+# ReSharper properties
+resharper_max_initializer_elements_on_line = 1
diff --git a/Directory.Build.props b/Directory.Build.props
index 3226242..02315ad 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,8 +4,8 @@
沙漠尽头的狼
CodeWF
Copyright (c) 2025 https://codewf.com
- 11.3.9
- 11.3.9
+ 11.3.11
+ 11.3.11
5.0.0-1.25277.114
true
MIT
@@ -18,4 +18,4 @@
enable
-
\ No newline at end of file
+
diff --git a/src/Lang.Avalonia.Analysis.Demo/App.axaml.cs b/src/Lang.Avalonia.Analysis.Demo/App.axaml.cs
index f3fe8fd..04f657b 100644
--- a/src/Lang.Avalonia.Analysis.Demo/App.axaml.cs
+++ b/src/Lang.Avalonia.Analysis.Demo/App.axaml.cs
@@ -12,7 +12,7 @@ public partial class App : Application
{
public override void Initialize()
{
- I18nManager.Instance.Register(new JsonLangPlugin(), new CultureInfo("zh-CN"), out _);
+ I18nManager.Instance.Register(new JsonLangPlugin(), new CultureInfo("zh-CN"));
base.Initialize(); // <-- Required
AvaloniaXamlLoader.Load(this);
}
@@ -29,4 +29,4 @@ public override void OnFrameworkInitializationCompleted()
base.OnFrameworkInitializationCompleted();
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Analysis.Demo/ViewModels/MainWindowViewModel.cs b/src/Lang.Avalonia.Analysis.Demo/ViewModels/MainWindowViewModel.cs
index de13c99..987a4a8 100644
--- a/src/Lang.Avalonia.Analysis.Demo/ViewModels/MainWindowViewModel.cs
+++ b/src/Lang.Avalonia.Analysis.Demo/ViewModels/MainWindowViewModel.cs
@@ -1,4 +1,4 @@
-using ReactiveUI;
+using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -29,7 +29,7 @@ public MainWindowViewModel()
});
}
- public List? Languages { get; set; }
+ public IReadOnlyCollection? Languages { get; set; }
public LocalizationLanguage? _selectLanguage;
public LocalizationLanguage? SelectLanguage
@@ -51,4 +51,4 @@ public DateTime CurrentTime
}
public ObservableCollection AllCultures { get; }
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Analysis/Lang.Avalonia.Analysis.csproj b/src/Lang.Avalonia.Analysis/Lang.Avalonia.Analysis.csproj
index 369c596..612443d 100644
--- a/src/Lang.Avalonia.Analysis/Lang.Avalonia.Analysis.csproj
+++ b/src/Lang.Avalonia.Analysis/Lang.Avalonia.Analysis.csproj
@@ -1,27 +1,27 @@
-
- netstandard2.0
- token(https://github.com/239573049);$(Authors)
-
+
+ netstandard2.0
+ token(https://github.com/239573049);$(Authors)
+
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
-
- true
-
- false
-
- true
-
-
-
-
+
+ true
+
+ false
+
+ true
+
+
+
+
diff --git a/src/Lang.Avalonia.Analysis/LanguageResourceParser.cs b/src/Lang.Avalonia.Analysis/LanguageResourceParser.cs
index 51d53be..cd5c0f6 100644
--- a/src/Lang.Avalonia.Analysis/LanguageResourceParser.cs
+++ b/src/Lang.Avalonia.Analysis/LanguageResourceParser.cs
@@ -18,23 +18,15 @@ public static Dictionary> ParseJsonFile(strin
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
- if (!IsValidJsonLanguageFile(root))
- return result;
-
- var cultureName = root.GetProperty("cultureName").GetString();
- if (string.IsNullOrEmpty(cultureName))
+ if (GetPropertyString(root, Consts.LanguageKey) is null
+ || GetPropertyString(root, Consts.DescriptionKey) is null
+ || GetPropertyString(root, Consts.CultureNameKey) is not { } cultureName)
return result;
var allProperties = new Dictionary();
CollectJsonProperties(root, "", allProperties);
- var excludeKeys = new[] { "language", "description", "cultureName" };
- var filteredProperties = allProperties
- .Where(kvp => !excludeKeys.Any(k => kvp.Key.Equals(k, StringComparison.OrdinalIgnoreCase)))
- .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
-
- if (!string.IsNullOrEmpty(cultureName))
- result[cultureName] = filteredProperties;
+ result[cultureName] = allProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
catch
{
@@ -42,6 +34,14 @@ public static Dictionary> ParseJsonFile(strin
}
return result;
+
+ static string? GetPropertyString(JsonElement element, string propertyName)
+ {
+ if (!element.TryGetProperty(propertyName, out var languageProp))
+ return null;
+ var value = languageProp.GetString();
+ return string.IsNullOrWhiteSpace(value) ? null : value;
+ }
}
public static Dictionary> ParseXmlFile(string filePath, string content)
@@ -52,17 +52,15 @@ public static Dictionary> ParseXmlFile(string
{
var doc = XDocument.Parse(content);
var root = doc.Root;
-
- if (root == null || !IsValidXmlLanguageFile(root))
- return result;
- var cultureName = root.Attribute("cultureName")?.Value;
- if (string.IsNullOrEmpty(cultureName))
+ if (GetAttributeString(root, Consts.LanguageKey) is null
+ || GetAttributeString(root, Consts.DescriptionKey) is null
+ || GetAttributeString(root, Consts.CultureNameKey) is not { } cultureName)
return result;
var properties = new Dictionary();
var propertyNodes = doc.Nodes().OfType().DescendantsAndSelf()
- .Where(e => e.Descendants().Any() != true).ToList();
+ .Where(e => !e.HasElements);
foreach (var propertyNode in propertyNodes)
{
@@ -71,8 +69,7 @@ public static Dictionary> ParseXmlFile(string
properties[key] = propertyNode.Value;
}
- if (!string.IsNullOrEmpty(cultureName))
- result[cultureName] = properties;
+ result[cultureName] = properties;
}
catch
{
@@ -80,6 +77,12 @@ public static Dictionary> ParseXmlFile(string
}
return result;
+
+ static string? GetAttributeString(XElement? element, string attributeName)
+ {
+ var value = element?.Attribute(attributeName)?.Value;
+ return string.IsNullOrWhiteSpace(value) ? null : value;
+ }
}
public static Dictionary> ParseResxFile(string filePath, string content)
@@ -99,7 +102,7 @@ public static Dictionary> ParseResxFile(strin
if (!string.IsNullOrEmpty(name) && valueElement != null)
{
- properties[name] = valueElement.Value ?? string.Empty;
+ properties[name!] = valueElement.Value ?? string.Empty;
}
}
@@ -117,20 +120,6 @@ public static Dictionary> ParseResxFile(strin
return result;
}
- private static bool IsValidJsonLanguageFile(JsonElement root)
- {
- return root.TryGetProperty("language", out _) &&
- root.TryGetProperty("description", out _) &&
- root.TryGetProperty("cultureName", out _);
- }
-
- private static bool IsValidXmlLanguageFile(XElement root)
- {
- return root.Attribute("language") != null &&
- root.Attribute("description") != null &&
- root.Attribute("cultureName") != null;
- }
-
private static void CollectJsonProperties(JsonElement element, string currentPath, Dictionary result)
{
switch (element.ValueKind)
@@ -147,7 +136,7 @@ private static void CollectJsonProperties(JsonElement element, string currentPat
break;
case JsonValueKind.Array:
- int index = 0;
+ var index = 0;
foreach (var item in element.EnumerateArray())
{
var newPath = $"{currentPath}[{index}]";
@@ -161,6 +150,13 @@ private static void CollectJsonProperties(JsonElement element, string currentPat
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Null:
+
+ // 过滤掉根节点的元数据属性
+ if (Consts.LanguageKey.Equals(currentPath, StringComparison.OrdinalIgnoreCase)
+ || Consts.DescriptionKey.Equals(currentPath, StringComparison.OrdinalIgnoreCase)
+ || Consts.CultureNameKey.Equals(currentPath, StringComparison.OrdinalIgnoreCase))
+ return;
+
result[currentPath] = element.ToString();
break;
}
@@ -201,4 +197,13 @@ public enum LanguageFileType
Json,
Xml,
Resx
-}
\ No newline at end of file
+}
+
+static file class Consts
+{
+ public const string LanguageKey = "language";
+
+ public const string DescriptionKey = "description";
+
+ public const string CultureNameKey = "cultureName";
+}
diff --git a/src/Lang.Avalonia.Analysis/LanguageSourceGenerator.cs b/src/Lang.Avalonia.Analysis/LanguageSourceGenerator.cs
index 40736e1..90bc249 100644
--- a/src/Lang.Avalonia.Analysis/LanguageSourceGenerator.cs
+++ b/src/Lang.Avalonia.Analysis/LanguageSourceGenerator.cs
@@ -49,11 +49,8 @@ private static void GenerateLanguageSource(SourceProductionContext context, Immu
var allResources = new Dictionary>();
- foreach (var file in files)
+ foreach (var (filePath, content) in files)
{
- var filePath = file.Path;
- var content = file.Content;
-
if (string.IsNullOrEmpty(content))
continue;
@@ -106,4 +103,4 @@ private static void GenerateLanguageSource(SourceProductionContext context, Immu
context.ReportDiagnostic(diagnostic);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Json.Demo/App.axaml.cs b/src/Lang.Avalonia.Json.Demo/App.axaml.cs
index 26e1ce2..29c21b1 100644
--- a/src/Lang.Avalonia.Json.Demo/App.axaml.cs
+++ b/src/Lang.Avalonia.Json.Demo/App.axaml.cs
@@ -12,7 +12,7 @@ public partial class App : PrismApplication
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
- I18nManager.Instance.Register(new JsonLangPlugin(), new CultureInfo("zh-CN"), out _);
+ I18nManager.Instance.Register(new JsonLangPlugin(), new CultureInfo("zh-CN"));
base.Initialize(); // <-- Required
}
@@ -34,4 +34,4 @@ protected override AvaloniaObject CreateShell()
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Json.Demo/ViewModels/MainViewModel.cs b/src/Lang.Avalonia.Json.Demo/ViewModels/MainViewModel.cs
index d68c7e2..1445c3f 100644
--- a/src/Lang.Avalonia.Json.Demo/ViewModels/MainViewModel.cs
+++ b/src/Lang.Avalonia.Json.Demo/ViewModels/MainViewModel.cs
@@ -18,7 +18,6 @@ public MainViewModel()
var titleCurrentCulture = I18nManager.Instance.GetResource(Localization.Main.MainView.Title);
var titleZhCN = I18nManager.Instance.GetResource(Localization.Main.MainView.Title, "zh-CN");
var titleEnUS = I18nManager.Instance.GetResource(Localization.Main.MainView.Title, "en-US");
-
Task.Run(async () =>
{
while (true)
@@ -29,7 +28,7 @@ public MainViewModel()
});
}
- public List? Languages { get; set; }
+ public IReadOnlyCollection? Languages { get; set; }
public LocalizationLanguage? _selectLanguage;
public LocalizationLanguage? SelectLanguage
@@ -51,4 +50,4 @@ public DateTime CurrentTime
}
public ObservableCollection AllCultures { get; }
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Json.Demo/Views/MainView.axaml b/src/Lang.Avalonia.Json.Demo/Views/MainView.axaml
index 5cbc7a4..a02d175 100644
--- a/src/Lang.Avalonia.Json.Demo/Views/MainView.axaml
+++ b/src/Lang.Avalonia.Json.Demo/Views/MainView.axaml
@@ -2,62 +2,59 @@
x:Class="Lang.Avalonia.Json.Demo.Views.MainView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:c="https://codewf.com"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:developModuleLanguage="clr-namespace:Localization.DevelopModule"
xmlns:globalization="clr-namespace:System.Globalization;assembly=mscorlib"
+ xmlns:mainLangs="clr-namespace:Localization.Main"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:u="https://irihi.tech/ursa"
- xmlns:mainLangs="clr-namespace:Localization.Main"
- xmlns:developModuleLanguage="clr-namespace:Localization.DevelopModule"
- xmlns:c="https://codewf.com"
xmlns:vm="clr-namespace:Lang.Avalonia.Json.Demo.ViewModels"
+ Margin="20"
d:DesignHeight="250"
- d:DesignWidth="400" Margin="20"
+ d:DesignWidth="400"
prism:ViewModelLocator.AutoWireViewModel="True"
x:DataType="vm:MainViewModel"
mc:Ignorable="d">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Lang.Avalonia.Json/JsonLangPlugin.cs b/src/Lang.Avalonia.Json/JsonLangPlugin.cs
index 3056555..f2491d6 100644
--- a/src/Lang.Avalonia.Json/JsonLangPlugin.cs
+++ b/src/Lang.Avalonia.Json/JsonLangPlugin.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
@@ -10,93 +10,85 @@ namespace Lang.Avalonia.Json;
public class JsonLangPlugin : ILangPlugin
{
- private readonly JsonSerializerOptions _jsonOptions = new()
- {
- PropertyNameCaseInsensitive = true
- };
+ public Dictionary Resources { get; } = new();
- public Dictionary Resources { get; private set; }= new();
public string ResourceFolder { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
- private CultureInfo _defaultCulture;
- public CultureInfo Culture { get; set; }
+ private LocalizationLanguage _defaultLanguage = null!;
+
+ public CultureInfo Culture { get; set; } = null!;
+
+ [MemberNotNull(nameof(Culture), nameof(_defaultLanguage))]
public void Load(CultureInfo cultureInfo)
{
- _defaultCulture = cultureInfo;
Culture = cultureInfo;
// 获取所有JSON文件并筛选有效语言文件
- var jsonFiles = Directory.GetFiles(ResourceFolder, "*.json", SearchOption.AllDirectories)
- .Where(IsValidLanguageFile)
- .ToList();
-
- if (!jsonFiles.Any())
- {
- Console.WriteLine("Please provide valid language JSON files");
- return;
- }
+ var jsonFiles = Directory.GetFiles(ResourceFolder, "*.json", SearchOption.AllDirectories);
foreach (var jsonFile in jsonFiles)
{
- using var doc = JsonDocument.Parse(File.ReadAllText(jsonFile));
- var root = doc.RootElement;
-
- // 解析根节点的元数据
- var language = new LocalizationLanguage
+ try
{
- Language = root.GetProperty("language").GetString()!,
- Description = root.GetProperty("description").GetString()!,
- CultureName = root.GetProperty("cultureName").GetString()!,
- };
+ using var doc = JsonDocument.Parse(File.ReadAllText(jsonFile));
+ var root = doc.RootElement;
- Resources.TryAdd(language.CultureName, language);
+ if (GetPropertyString(root, Consts.LanguageKey) is not { } language
+ || GetPropertyString(root, Consts.DescriptionKey) is not { } description
+ || GetPropertyString(root, Consts.CultureNameKey) is not { } cultureName)
+ continue;
- // 递归收集所有键值对(排除根节点的三个元数据属性)
- var allProperties = new Dictionary();
- CollectJsonProperties(root, "", allProperties);
+ // 解析根节点的元数据
+ var localizationLanguage = new LocalizationLanguage
+ {
+ Language = language,
+ Description = description,
+ CultureName = cultureName,
+ };
- // 过滤掉根节点的元数据属性
- var excludeKeys = new[] { "language", "description", "cultureName" };
- foreach (var (key, value) in allProperties)
+ Resources.TryAdd(localizationLanguage.CultureName, localizationLanguage);
+
+ if (localizationLanguage.CultureName == cultureInfo.Name)
+ _defaultLanguage = localizationLanguage;
+
+ // 递归收集所有键值对(排除根节点的三个元数据属性)
+ CollectJsonProperties(root, "", localizationLanguage.Languages);
+ }
+ catch
{
- if (!excludeKeys.Any(k => key.Equals(k, StringComparison.OrdinalIgnoreCase)))
- {
- Resources[language.CultureName].Languages[key] = value;
- }
+ // ignored
}
}
- }
- public void AddResource(params Assembly[] assemblies)
- {
- throw new NotImplementedException(nameof(AddResource));
- }
+ if (_defaultLanguage is null)
+ throw new InvalidDataException("Missing default culture resources");
- // 验证JSON文件是否包含必要的根属性
- private bool IsValidLanguageFile(string filePath)
- {
- try
- {
- using var doc = JsonDocument.Parse(File.ReadAllText(filePath));
- var root = doc.RootElement;
+ return;
- return root.TryGetProperty("language", out _)
- && root.TryGetProperty("description", out _)
- && root.TryGetProperty("cultureName", out _);
- }
- catch
+ static string? GetPropertyString(JsonElement element, string propertyName)
{
- return false;
+ if (!element.TryGetProperty(propertyName, out var languageProp))
+ return null;
+ var value = languageProp.GetString();
+ return string.IsNullOrWhiteSpace(value) ? null : value;
}
}
- // 递归遍历JSON元素,收集所有键值对(生成类似XML的层级键)
- private void CollectJsonProperties(JsonElement element, string currentPath, Dictionary result)
+ public void AddResource(params IEnumerable assemblies) =>
+ throw new NotSupportedException(nameof(AddResource));
+
+ ///
+ /// 递归遍历JSON元素,收集所有键值对(生成类似XML的层级键)
+ ///
+ ///
+ ///
+ ///
+ private static void CollectJsonProperties(JsonElement element, string currentPath, Dictionary result)
{
switch (element.ValueKind)
{
+ // 遍历对象的所有属性
case JsonValueKind.Object:
- // 遍历对象的所有属性
foreach (var property in element.EnumerateObject())
{
var newPath = string.IsNullOrEmpty(currentPath)
@@ -107,9 +99,9 @@ private void CollectJsonProperties(JsonElement element, string currentPath, Dict
}
break;
+ // 处理数组(按索引拼接键名)
case JsonValueKind.Array:
- // 处理数组(按索引拼接键名)
- int index = 0;
+ var index = 0;
foreach (var item in element.EnumerateArray())
{
var newPath = $"{currentPath}[{index}]";
@@ -118,38 +110,36 @@ private void CollectJsonProperties(JsonElement element, string currentPath, Dict
}
break;
+ // 处理基本类型值
case JsonValueKind.String:
case JsonValueKind.Number:
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Null:
- // 处理基本类型值
+
+ // 过滤掉根节点的元数据属性
+ if (Consts.LanguageKey.Equals(currentPath, StringComparison.OrdinalIgnoreCase)
+ || Consts.DescriptionKey.Equals(currentPath, StringComparison.OrdinalIgnoreCase)
+ || Consts.CultureNameKey.Equals(currentPath, StringComparison.OrdinalIgnoreCase))
+ return;
+
result[currentPath] = element.ToString();
break;
}
}
- public List? GetLanguages() => Resources?.Select(kvp => kvp.Value).ToList();
+ public IReadOnlyCollection GetLanguages() => Resources.Values;
- public string? GetResource(string key, string? cultureName = null)
+ public string GetResource(string key, string? cultureName = null)
{
- var culture = Culture?.Name ?? string.Empty;
- if (!string.IsNullOrWhiteSpace(cultureName))
- {
- culture = cultureName;
- }
+ ((ILangPlugin) this).EnsureLoaded();
- if (Resources?.TryGetValue(culture, out var currentLanguages) == true
- && currentLanguages.Languages.TryGetValue(key, out string resource))
- {
- return resource;
- }
- if (Resources?.TryGetValue(_defaultCulture.Name, out currentLanguages) == true
- && currentLanguages.Languages.TryGetValue(key, out resource))
- {
+ if (string.IsNullOrWhiteSpace(cultureName))
+ cultureName = Culture.Name;
+
+ if (((ILangPlugin) this).GetResourceInternal(cultureName, key, out var resource))
return resource;
- }
- return key;
+ return _defaultLanguage.Languages.GetValueOrDefault(key, key);
}
}
diff --git a/src/Lang.Avalonia.Json/Lang.Avalonia.Json.csproj b/src/Lang.Avalonia.Json/Lang.Avalonia.Json.csproj
index fa285cd..2e844b4 100644
--- a/src/Lang.Avalonia.Json/Lang.Avalonia.Json.csproj
+++ b/src/Lang.Avalonia.Json/Lang.Avalonia.Json.csproj
@@ -2,7 +2,7 @@
Lang.Avalonia.Json
- net8.0;net9.0;net10.0
+ net6.0;net8.0;net9.0;net10.0
Library
true
Avalonia UI 国际化解决方案的 Json 插件,提供 Json 格式的多语言资源加载支持
@@ -12,18 +12,7 @@
git
true
README.md
-
-
-
- true
-
-
-
- true
-
-
-
- true
+ true
diff --git a/src/Lang.Avalonia.Resx.Demo/App.axaml.cs b/src/Lang.Avalonia.Resx.Demo/App.axaml.cs
index 52ca055..9b66733 100644
--- a/src/Lang.Avalonia.Resx.Demo/App.axaml.cs
+++ b/src/Lang.Avalonia.Resx.Demo/App.axaml.cs
@@ -12,7 +12,7 @@ public partial class App : PrismApplication
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
- I18nManager.Instance.Register(new ResxLangPlugin(), new CultureInfo("zh-CN"), out _);
+ I18nManager.Instance.Register(new ResxLangPlugin(), new CultureInfo("zh-CN"));
base.Initialize(); // <-- Required
}
@@ -34,4 +34,4 @@ protected override AvaloniaObject CreateShell()
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Resx.Demo/MainWindow.axaml.cs b/src/Lang.Avalonia.Resx.Demo/MainWindow.axaml.cs
index 6e9091f..844d50d 100644
--- a/src/Lang.Avalonia.Resx.Demo/MainWindow.axaml.cs
+++ b/src/Lang.Avalonia.Resx.Demo/MainWindow.axaml.cs
@@ -9,4 +9,4 @@ public MainWindow()
InitializeComponent();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Resx.Demo/ViewModels/MainViewModel.cs b/src/Lang.Avalonia.Resx.Demo/ViewModels/MainViewModel.cs
index 1f04c3f..948cbf8 100644
--- a/src/Lang.Avalonia.Resx.Demo/ViewModels/MainViewModel.cs
+++ b/src/Lang.Avalonia.Resx.Demo/ViewModels/MainViewModel.cs
@@ -11,14 +11,33 @@ public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
- Languages = new List()
- {
- new(){ CultureName = "en-US", Description = "English", Language = "English"},
- new(){ CultureName = "zh-CN", Description = "Chinese (Simplified)", Language = "Chinese (Simplified)"},
- new(){ CultureName = "zh-Hant", Description = "Chinese (Traditional)", Language = "Chinese (Traditional)"},
- new(){ CultureName = "ja-JP", Description = "Japanese", Language = "Japanese"}
-
- };
+ Languages =
+ [
+ new()
+ {
+ CultureName = "en-US",
+ Description = "English",
+ Language = "English"
+ },
+ new()
+ {
+ CultureName = "zh-CN",
+ Description = "Chinese (Simplified)",
+ Language = "Chinese (Simplified)"
+ },
+ new()
+ {
+ CultureName = "zh-Hant",
+ Description = "Chinese (Traditional)",
+ Language = "Chinese (Traditional)"
+ },
+ new()
+ {
+ CultureName = "ja-JP",
+ Description = "Japanese",
+ Language = "Japanese"
+ }
+ ];
SelectLanguage = Languages?.FirstOrDefault(l => l.CultureName == I18nManager.Instance.Culture.Name);
var titleCurrentCulture = I18nManager.Instance.GetResource(Localization.Main.MainView.Title);
@@ -55,4 +74,4 @@ public DateTime CurrentTime
get => _currentTime;
set => this.RaiseAndSetIfChanged(ref _currentTime, value);
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Resx/Lang.Avalonia.Resx.csproj b/src/Lang.Avalonia.Resx/Lang.Avalonia.Resx.csproj
index bdaa30b..4b24b42 100644
--- a/src/Lang.Avalonia.Resx/Lang.Avalonia.Resx.csproj
+++ b/src/Lang.Avalonia.Resx/Lang.Avalonia.Resx.csproj
@@ -2,7 +2,7 @@
Lang.Avalonia.Resx
- net8.0;net9.0;net10.0
+ net6.0;net8.0;net9.0;net10.0
Library
true
Avalonia UI 国际化解决方案的 Resx 插件,提供 Resx 格式的多语言资源加载支持
@@ -12,18 +12,7 @@
git
true
README.md
-
-
-
- true
-
-
-
- true
-
-
-
- true
+ false
diff --git a/src/Lang.Avalonia.Resx/ResxLangPlugin.cs b/src/Lang.Avalonia.Resx/ResxLangPlugin.cs
index 4fae164..9d8deba 100644
--- a/src/Lang.Avalonia.Resx/ResxLangPlugin.cs
+++ b/src/Lang.Avalonia.Resx/ResxLangPlugin.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
@@ -8,13 +9,16 @@
namespace Lang.Avalonia.Resx;
+[RequiresUnreferencedCode("Type.GetProperty")]
public class ResxLangPlugin : ILangPlugin
{
public Dictionary Resources { get; } = new();
+
public string Mark { get; set; } = "i18n";
+
private Dictionary? _resourceManagers;
- private CultureInfo _defaultCulture;
+ private CultureInfo _defaultCulture = null!;
public CultureInfo Culture
{
@@ -24,137 +28,72 @@ public CultureInfo Culture
field = value;
Sync(value);
}
- }
+ } = null!;
public void Load(CultureInfo cultureInfo)
{
_defaultCulture = cultureInfo;
Culture = cultureInfo;
- _resourceManagers = AppDomain.CurrentDomain.GetAssemblies()
- .SelectMany(assembly =>
- assembly.GetTypes()
- .Where(type => type.FullName.Contains(Mark, StringComparison.OrdinalIgnoreCase))
- .ToDictionary(
- type => type,
- type => type.GetProperty(nameof(ResourceManager),
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
- ?.GetValue(null, null) as ResourceManager)
- )
- .Where(pair => pair.Value != null)
- .ToDictionary(pair => pair.Key, pair => pair.Value!);
+ _resourceManagers = GetFromAssemblies(AppDomain.CurrentDomain.GetAssemblies());
Sync(Culture);
}
- public void AddResource(params Assembly[] assemblies)
+ public void AddResource(params IEnumerable assemblies)
{
- var dicts = assemblies.SelectMany(assembly =>
- assembly.GetTypes()
- .Where(type => type.FullName.Contains(Mark, StringComparison.OrdinalIgnoreCase))
- .ToDictionary(
- type => type,
- type => type.GetProperty(nameof(ResourceManager),
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
- ?.GetValue(null, null) as ResourceManager)
- )
- .Where(pair => pair.Value != null)
- .ToDictionary(pair => pair.Key, pair => pair.Value!);
- if (dicts.Count != 0)
- {
- foreach (KeyValuePair pair in dicts)
- {
- if (!_resourceManagers.ContainsKey(pair.Key))
- {
- _resourceManagers.Add(pair.Key, pair.Value);
- }
- }
- }
+ ((ILangPlugin) this).EnsureLoaded();
+
+ var dict = GetFromAssemblies(assemblies);
+ foreach (var pair in dict)
+ _resourceManagers!.TryAdd(pair.Key, pair.Value);
Sync(Culture);
}
- public List? GetLanguages() =>
+ private Dictionary GetFromAssemblies(IEnumerable assemblies) =>
+ assemblies.SelectMany(assembly =>
+ assembly.GetTypes()
+ .Where(type => type.FullName?.Contains(Mark, StringComparison.OrdinalIgnoreCase) ?? false))
+ .Select(type => (Type: type, Property: type.GetProperty(nameof(ResourceManager),
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
+ ?.GetValue(null, null) as ResourceManager))
+ .Where(pair => pair.Property is not null)
+ .ToDictionary(pair => pair.Type, pair => pair.Property!);
+
+ public IReadOnlyCollection GetLanguages() =>
throw new NotSupportedException("This plugin does not support the current interface for the time being.");
- public string? GetResource(string key, string? cultureName = null)
+ public string GetResource(string key, string? cultureName = null)
{
- var culture = Culture.Name;
+ ((ILangPlugin) this).EnsureLoaded();
- string? GetResource()
- {
- if (Resources.TryGetValue(culture, out var currentLanguages)
- && currentLanguages.Languages.TryGetValue(key, out string resource))
- {
- return resource;
- }
+ if (string.IsNullOrWhiteSpace(cultureName))
+ cultureName = Culture.Name;
- return default;
- }
+ if (((ILangPlugin) this).GetResourceInternal(cultureName, key, out var resource))
+ return resource;
- if (!string.IsNullOrWhiteSpace(cultureName))
- {
- culture = cultureName;
- }
+ Sync(new CultureInfo(cultureName));
- bool isFirst = true;
- var resource = GetResource();
- if (!string.IsNullOrWhiteSpace(resource))
- {
+ if (((ILangPlugin) this).GetResourceInternal(cultureName, key, out resource))
return resource;
- }
- Sync(new CultureInfo(culture));
- resource = GetResource();
- if (!string.IsNullOrWhiteSpace(resource))
- {
- return resource;
- }
+ cultureName = _defaultCulture.Name;
- culture = _defaultCulture.Name;
- resource = GetResource();
- if (!string.IsNullOrWhiteSpace(resource))
- {
+ if (((ILangPlugin) this).GetResourceInternal(cultureName, key, out resource))
return resource;
- }
return key;
}
-
private void Sync(CultureInfo cultureInfo)
{
- if (_resourceManagers == null || _resourceManagers.Count == 0)
- {
+ if (_resourceManagers is not { Count: > 0 })
return;
- }
-
- IEnumerable GetResources(ResourceManager resourceManager)
- {
- var baseEntries = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true)
- ?.OfType();
- var cultureEntries = resourceManager.GetResourceSet(cultureInfo, true, true)?.OfType();
- if (cultureEntries == null || baseEntries == null)
- {
- yield break;
- }
-
- foreach (var entry in cultureEntries
- .Concat(baseEntries)
- .GroupBy(entry => entry.Key)
- .Select(entries => entries.First()))
- {
- yield return entry;
- }
- }
var cultureName = cultureInfo.Name;
- LocalizationLanguage? currentLanResources;
- if (Resources.ContainsKey(cultureName))
- {
- currentLanResources = Resources[cultureName];
- }
- else
+ if (!Resources.TryGetValue(cultureName, out var currentLanResources))
{
- currentLanResources = new LocalizationLanguage()
+ currentLanResources = new()
{
Language = cultureInfo.DisplayName,
Description = cultureInfo.DisplayName,
@@ -166,13 +105,28 @@ IEnumerable GetResources(ResourceManager resourceManager)
foreach (var pair in _resourceManagers)
{
pair.Key.GetProperty("Culture", BindingFlags.Public | BindingFlags.Static)?.SetValue(null, cultureInfo);
- foreach (var entry in GetResources(pair.Value))
+ foreach (var entry in GetResourcesInternal(pair.Value))
{
- if (entry.Key is string key && entry.Value is string value)
+ if (entry is { Key: string key, Value: string value })
{
currentLanResources.Languages[key] = value;
}
}
}
+
+ return;
+
+ IEnumerable GetResourcesInternal(ResourceManager resourceManager)
+ {
+ var baseEntries = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true)
+ ?.OfType();
+ var cultureEntries = resourceManager.GetResourceSet(cultureInfo, true, true)?.OfType();
+ if (cultureEntries is null || baseEntries is null)
+ return [];
+
+ return cultureEntries
+ .Concat(baseEntries)
+ .DistinctBy(entry => entry.Key);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Xml.Demo/App.axaml.cs b/src/Lang.Avalonia.Xml.Demo/App.axaml.cs
index e2e3941..25bad1d 100644
--- a/src/Lang.Avalonia.Xml.Demo/App.axaml.cs
+++ b/src/Lang.Avalonia.Xml.Demo/App.axaml.cs
@@ -12,7 +12,7 @@ public partial class App : PrismApplication
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
- I18nManager.Instance.Register(new XmlLangPlugin(), new CultureInfo("zh-CN"), out _);
+ I18nManager.Instance.Register(new XmlLangPlugin(), new CultureInfo("zh-CN"));
base.Initialize(); // <-- Required
}
@@ -34,4 +34,4 @@ protected override AvaloniaObject CreateShell()
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Xml.Demo/ViewModels/MainViewModel.cs b/src/Lang.Avalonia.Xml.Demo/ViewModels/MainViewModel.cs
index 068d22d..809076d 100644
--- a/src/Lang.Avalonia.Xml.Demo/ViewModels/MainViewModel.cs
+++ b/src/Lang.Avalonia.Xml.Demo/ViewModels/MainViewModel.cs
@@ -29,7 +29,7 @@ public MainViewModel()
});
}
- public List? Languages { get; set; }
+ public IReadOnlyCollection? Languages { get; set; }
public LocalizationLanguage? _selectLanguage;
public LocalizationLanguage? SelectLanguage
@@ -49,4 +49,4 @@ public DateTime CurrentTime
get => _currentTime;
set => this.RaiseAndSetIfChanged(ref _currentTime, value);
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia.Xml/Lang.Avalonia.Xml.csproj b/src/Lang.Avalonia.Xml/Lang.Avalonia.Xml.csproj
index c0a0639..8693bc4 100644
--- a/src/Lang.Avalonia.Xml/Lang.Avalonia.Xml.csproj
+++ b/src/Lang.Avalonia.Xml/Lang.Avalonia.Xml.csproj
@@ -2,7 +2,7 @@
Lang.Avalonia.Xml
- net8.0;net9.0;net10.0
+ net6.0;net8.0;net9.0;net10.0
Library
true
Avalonia UI 国际化解决方案的 XML 插件,提供 XML 格式的多语言资源加载支持
@@ -12,18 +12,7 @@
git
true
README.md
-
-
-
- true
-
-
-
- true
-
-
-
- true
+ true
diff --git a/src/Lang.Avalonia.Xml/XmlLangPlugin.cs b/src/Lang.Avalonia.Xml/XmlLangPlugin.cs
index e0f034d..07f36ce 100644
--- a/src/Lang.Avalonia.Xml/XmlLangPlugin.cs
+++ b/src/Lang.Avalonia.Xml/XmlLangPlugin.cs
@@ -1,5 +1,6 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -11,93 +12,87 @@ namespace Lang.Avalonia.Xml;
public class XmlLangPlugin : ILangPlugin
{
public Dictionary Resources { get; } = new();
+
public string ResourceFolder { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
- private CultureInfo _defaultCulture;
- public CultureInfo Culture { get; set; }
+ private LocalizationLanguage _defaultLanguage = null!;
+
+ public CultureInfo Culture { get; set; } = null!;
+ [MemberNotNull(nameof(Culture), nameof(_defaultLanguage))]
public void Load(CultureInfo cultureInfo)
{
- _defaultCulture = cultureInfo;
Culture = cultureInfo;
- LocalizationLanguage ReadLanguage(XElement element)
+ var xmlFiles = Directory.GetFiles(ResourceFolder, "*.xml", SearchOption.AllDirectories);
+
+ foreach (var xmlFile in xmlFiles)
{
- return new LocalizationLanguage
+ try
{
- Language = element!.Attribute(Consts.LanguageKey)!.Value,
- Description = element.Attribute("description")!.Value,
- CultureName = element.Attribute("cultureName")!.Value
- };
- }
+ var xmlDoc = XDocument.Load(xmlFile);
- var xmlFiles = Directory.GetFiles(ResourceFolder, "*.xml", SearchOption.AllDirectories)
- .Where(file =>
- {
- var doc = XDocument.Load(file);
- var root = doc.Root;
- var language = root?.Attribute(Consts.LanguageKey)?.Value;
- var description = root?.Attribute(Consts.DescriptionKey)?.Value;
- var cultureName = root?.Attribute(Consts.CultureNameKey)?.Value;
- return !string.IsNullOrWhiteSpace(language)
- && !string.IsNullOrWhiteSpace(description)
- && !string.IsNullOrWhiteSpace(cultureName);
- }).ToList();
-
- if (xmlFiles.Any() != true)
- {
- Console.WriteLine("Please provide the language XML file");
- return;
- }
+ var root = xmlDoc.Root;
- foreach (var xmlFile in xmlFiles)
- {
- var xmlDoc = XDocument.Load(xmlFile);
+ if (GetAttributeString(root, Consts.LanguageKey) is not { } language
+ || GetAttributeString(root, Consts.DescriptionKey) is not { } description
+ || GetAttributeString(root, Consts.CultureNameKey) is not { } cultureName)
+ continue;
- var language = ReadLanguage(xmlDoc.Root!);
- if (!Resources.ContainsKey(language.CultureName))
- {
- Resources[language.CultureName] = language;
- }
+ var localizationLanguage = new LocalizationLanguage
+ {
+ Language = language,
+ Description = description,
+ CultureName = cultureName
+ };
+
+ Resources.TryAdd(localizationLanguage.CultureName, localizationLanguage);
+
+ if (localizationLanguage.CultureName == cultureInfo.Name)
+ _defaultLanguage = localizationLanguage;
+
+ var propertyNodes = xmlDoc.Nodes().OfType().DescendantsAndSelf()
+ .Where(e => !e.HasElements);
- var propertyNodes = xmlDoc.Nodes().OfType().DescendantsAndSelf()
- .Where(e => e.Descendants().Any() != true).ToList();
- foreach (var propertyNode in propertyNodes)
+ foreach (var propertyNode in propertyNodes)
+ {
+ var ancestorsNodeNames = propertyNode.AncestorsAndSelf().Reverse().Select(node => node.Name.LocalName);
+ var key = string.Join('.', ancestorsNodeNames);
+ Resources[localizationLanguage.CultureName].Languages[key] = propertyNode.Value;
+ }
+ }
+ catch
{
- var ancestorsNodeNames = propertyNode.AncestorsAndSelf().Reverse().Select(node => node.Name.LocalName);
- var key = string.Join(".", ancestorsNodeNames);
- Resources[language.CultureName].Languages[key] = propertyNode.Value;
+ // ignored
}
}
- }
- public void AddResource(params Assembly[] assemblies)
- {
- throw new NotImplementedException(nameof(AddResource));
- }
+ if (_defaultLanguage is null)
+ throw new InvalidDataException("Missing default culture resources");
- public List? GetLanguages() => Resources.Select(kvp => kvp.Value).ToList();
+ return;
- public string? GetResource(string key, string? cultureName = null)
- {
- var culture = Culture.Name;
- if (!string.IsNullOrWhiteSpace(cultureName))
+ static string? GetAttributeString(XElement? element, string attributeName)
{
- culture = cultureName;
+ var value = element?.Attribute(attributeName)?.Value;
+ return string.IsNullOrWhiteSpace(value) ? null : value;
}
+ }
- if (Resources.TryGetValue(culture, out var currentLanguages)
- && currentLanguages.Languages.TryGetValue(key, out string resource))
- {
- return resource;
- }
+ public void AddResource(params IEnumerable assemblies) => throw new NotSupportedException(nameof(AddResource));
- if (Resources?.TryGetValue(_defaultCulture.Name, out currentLanguages) == true
- && currentLanguages.Languages.TryGetValue(key, out resource))
- {
+ public IReadOnlyCollection GetLanguages() => Resources.Values;
+
+ public string GetResource(string key, string? cultureName = null)
+ {
+ ((ILangPlugin) this).EnsureLoaded();
+
+ if (string.IsNullOrWhiteSpace(cultureName))
+ cultureName = Culture.Name;
+
+ if (((ILangPlugin) this).GetResourceInternal(cultureName, key, out var resource))
return resource;
- }
- return key;
+ return _defaultLanguage.Languages.GetValueOrDefault(key, key);
}
-}
\ No newline at end of file
+}
diff --git a/src/Lang.Avalonia/AssemblyInfo.cs b/src/Lang.Avalonia/AssemblyInfo.cs
index e916c77..845f17f 100644
--- a/src/Lang.Avalonia/AssemblyInfo.cs
+++ b/src/Lang.Avalonia/AssemblyInfo.cs
@@ -1,6 +1,10 @@
-using Avalonia.Metadata;
+using System.Runtime.CompilerServices;
+using Avalonia.Metadata;
[assembly: XmlnsPrefix("Lang.Avalonia", "c")]
[assembly: XmlnsDefinition("https://codewf.com", "Lang.Avalonia")]
[assembly: XmlnsDefinition("https://codewf.com", "Lang.Avalonia.Markups")]
-[assembly: XmlnsDefinition("https://codewf.com", "Lang.Avalonia.MarkupExtensions")]
\ No newline at end of file
+[assembly: XmlnsDefinition("https://codewf.com", "Lang.Avalonia.MarkupExtensions")]
+[assembly: InternalsVisibleTo("Lang.Avalonia.Json")]
+[assembly: InternalsVisibleTo("Lang.Avalonia.Resx")]
+[assembly: InternalsVisibleTo("Lang.Avalonia.Xml")]
diff --git a/src/Lang.Avalonia/Consts.cs b/src/Lang.Avalonia/Consts.cs
index 878555e..943e456 100644
--- a/src/Lang.Avalonia/Consts.cs
+++ b/src/Lang.Avalonia/Consts.cs
@@ -1,8 +1,10 @@
-namespace Lang.Avalonia;
+namespace Lang.Avalonia;
-public static class Consts
+internal static class Consts
{
public const string LanguageKey = "language";
+
public const string DescriptionKey = "description";
+
public const string CultureNameKey = "cultureName";
}
diff --git a/src/Lang.Avalonia/Converters/I18nConverter.cs b/src/Lang.Avalonia/Converters/I18nConverter.cs
index 51556c1..c893308 100644
--- a/src/Lang.Avalonia/Converters/I18nConverter.cs
+++ b/src/Lang.Avalonia/Converters/I18nConverter.cs
@@ -1,36 +1,37 @@
-using Avalonia.Data.Converters;
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Globalization;
using System.Linq;
+using Avalonia.Data.Converters;
using Lang.Avalonia.MarkupExtensions;
namespace Lang.Avalonia.Converters;
-public class I18nConverter(I18nBinding owner) : IMultiValueConverter
+[EditorBrowsable(EditorBrowsableState.Never)]
+public class I18nConverter : IMultiValueConverter
{
public object? Convert(IList