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 values, Type targetType, object? parameter, CultureInfo culture) { - if (values[0] is not CultureInfo _) - { - return default; - } - - var value = values[1]; - if (owner.KeyConverter.Convert(value, null, null, culture) is not string key) - { + if (values is not [CultureInfo, var value, ..] || parameter is not I18nBinding owner) + return null; + + if (owner.KeyConverter is { } keyConverter) + value = keyConverter.Convert(value, typeof(string), null, culture); + + if (value is not string key) return value; - } - value = I18nManager.Instance.GetResource(key, owner.CultureName) ?? key; + value = I18nManager.Instance.GetResource(key, owner.CultureName); if (value is string format) - { - value = string.Format(format, owner.Args.Indexes + value = string.Format(format, owner.Indexes .Select(item => item.IsBinding ? values[item.Index] : owner.Args[item.Index]) .ToArray()); - } - return owner.ValueConverter.Convert(value, null, null, culture); + if (owner.ValueConverter is { } valueConverter) + return valueConverter.Convert(value, typeof(string), null, culture); + + return value; } } diff --git a/src/Lang.Avalonia/Converters/I18nKeyConverter.cs b/src/Lang.Avalonia/Converters/I18nKeyConverter.cs index 89213ad..a2a5cf3 100644 --- a/src/Lang.Avalonia/Converters/I18nKeyConverter.cs +++ b/src/Lang.Avalonia/Converters/I18nKeyConverter.cs @@ -1,12 +1,14 @@ -using Avalonia.Data.Converters; using System; +using System.ComponentModel; using System.Globalization; +using Avalonia.Data.Converters; namespace Lang.Avalonia.Converters; +[EditorBrowsable(EditorBrowsableState.Never)] public class I18nKeyConverter : IValueConverter { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { return Get(value); } @@ -16,7 +18,7 @@ public object ConvertBack(object? value, Type targetType, object? parameter, Cul throw new NotSupportedException(); } - public static string? Get(object? value) + public static string Get(object? value) { return value switch { diff --git a/src/Lang.Avalonia/Converters/I18nValueConverter.cs b/src/Lang.Avalonia/Converters/I18nValueConverter.cs deleted file mode 100644 index c75457e..0000000 --- a/src/Lang.Avalonia/Converters/I18nValueConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Avalonia.Data.Converters; -using System; -using System.Globalization; - -namespace Lang.Avalonia.Converters; - -public class I18nValueConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return Get(value); - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } - - public static object? Get(object? value, bool trimEnd = false) - { - return value switch - { - string @string => trimEnd ? @string.TrimEnd() : @string, - _ => value - }; - } -} diff --git a/src/Lang.Avalonia/I18nManager.cs b/src/Lang.Avalonia/I18nManager.cs index d7c3a16..310ef0c 100644 --- a/src/Lang.Avalonia/I18nManager.cs +++ b/src/Lang.Avalonia/I18nManager.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Threading; @@ -19,6 +20,14 @@ private I18nManager() { } + public void Register(ILangPlugin plugin, CultureInfo? defaultCulture = null) + { + ArgumentNullException.ThrowIfNull(plugin); + _langPlugin = plugin; + Culture = defaultCulture ?? CultureInfo.CurrentUICulture; + plugin.Load(Culture); + } + public bool Register(ILangPlugin plugin, CultureInfo defaultCulture, out string? error) { error = null; @@ -37,34 +46,53 @@ public bool Register(ILangPlugin plugin, CultureInfo defaultCulture, out string? } } - public void AddResource(params Assembly[] assemblies) + public void AddResource(params IEnumerable assemblies) { - _langPlugin?.AddResource(assemblies); + EnsureRegistered(); + ArgumentNullException.ThrowIfNull(assemblies); + _langPlugin.AddResource(assemblies); } - - public CultureInfo? Culture + public CultureInfo Culture { - get => _langPlugin?.Culture; + get + { + EnsureRegistered(); + return _langPlugin.Culture; + } set { - if (_langPlugin == null || Equals(_langPlugin?.Culture, value)) - { + EnsureRegistered(); + ArgumentNullException.ThrowIfNull(value); + if (Equals(_langPlugin.Culture, value)) return; - } - _langPlugin!.Culture = value; + _langPlugin.Culture = value; Thread.CurrentThread.CurrentCulture = value; Thread.CurrentThread.CurrentUICulture = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Culture))); + PropertyChanged?.Invoke(this, new(nameof(Culture))); CultureChanged?.Invoke(this, EventArgs.Empty); } } - public List? GetLanguages() => _langPlugin?.GetLanguages(); - + public IReadOnlyCollection GetLanguages() + { + EnsureRegistered(); + return _langPlugin.GetLanguages(); + } - public string? GetResource(string key, string? cultureName = null) => _langPlugin?.GetResource(key, cultureName); + public string GetResource(string key, string? cultureName = null) + { + EnsureRegistered(); + return _langPlugin.GetResource(key, cultureName); + } public event EventHandler? CultureChanged; -} \ No newline at end of file + + [MemberNotNull(nameof(_langPlugin))] + private void EnsureRegistered() + { + if (_langPlugin is null) + throw new InvalidOperationException($"{nameof(I18nManager)} is not registered. Please register a language plugin before using it."); + } +} diff --git a/src/Lang.Avalonia/ILangPlugin.cs b/src/Lang.Avalonia/ILangPlugin.cs index 3e507fa..fac5d80 100644 --- a/src/Lang.Avalonia/ILangPlugin.cs +++ b/src/Lang.Avalonia/ILangPlugin.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; @@ -6,9 +8,29 @@ namespace Lang.Avalonia; public interface ILangPlugin { + internal Dictionary Resources { get; } + void Load(CultureInfo cultureInfo); - void AddResource(params Assembly[] assemblies); - public CultureInfo Culture { get; set; } - List? GetLanguages(); - string? GetResource(string key, string? cultureName = null); -} \ No newline at end of file + + void AddResource(params IEnumerable assemblies); + + CultureInfo Culture { get; set; } + + IReadOnlyCollection GetLanguages(); + + string GetResource(string key, string? cultureName = null); + + [MemberNotNull(nameof(Culture))] + internal void EnsureLoaded() + { + if (Culture is null) + throw new InvalidOperationException($"Please call {nameof(Load)} method before using the plugin."); + } + + internal bool GetResourceInternal(string culture, string key, [NotNullWhen(true)] out string? result) + { + result = null; + return Resources.TryGetValue(culture, out var currentLanguages) + && currentLanguages.Languages.TryGetValue(key, out result); + } +} diff --git a/src/Lang.Avalonia/Lang.Avalonia.csproj b/src/Lang.Avalonia/Lang.Avalonia.csproj index 7b44b8e..bf50617 100644 --- a/src/Lang.Avalonia/Lang.Avalonia.csproj +++ b/src/Lang.Avalonia/Lang.Avalonia.csproj @@ -2,7 +2,7 @@ Lang.Avalonia - net8.0;net9.0;net10.0 + net6.0;net8.0;net9.0;net10.0 Library true Avalonia UI 国际化解决方案,提供多语言支持和资源管理 @@ -12,18 +12,7 @@ git true README.md - - - - true - - - - true - - - - true + true diff --git a/src/Lang.Avalonia/LocalizationLanguage.cs b/src/Lang.Avalonia/LocalizationLanguage.cs index cf57703..15775e8 100644 --- a/src/Lang.Avalonia/LocalizationLanguage.cs +++ b/src/Lang.Avalonia/LocalizationLanguage.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Lang.Avalonia; public class LocalizationLanguage { public string Language { get; set; } = null!; + public string Description { get; set; } = null!; + public string CultureName { get; set; } = null!; + public Dictionary Languages { get; } = new(); -} \ No newline at end of file +} diff --git a/src/Lang.Avalonia/MarkupExtensions/ArgCollection.cs b/src/Lang.Avalonia/MarkupExtensions/ArgCollection.cs deleted file mode 100644 index a3c27bc..0000000 --- a/src/Lang.Avalonia/MarkupExtensions/ArgCollection.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.Data; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Lang.Avalonia.MarkupExtensions; - -public class ArgCollection(I18nBinding owner) : Collection -{ - internal List<(bool IsBinding, int Index)> Indexes { get; } = []; - - protected override void InsertItem(int index, object item) - { - if (item is BindingBase binding) - { - Indexes.Add((true, owner.Bindings.Count)); - owner.Bindings.Add(binding); - } - else - { - Indexes.Add((false, Count)); - base.InsertItem(index, item); - } - } -} diff --git a/src/Lang.Avalonia/MarkupExtensions/I18nBinding.cs b/src/Lang.Avalonia/MarkupExtensions/I18nBinding.cs index ff9a05e..3405f16 100644 --- a/src/Lang.Avalonia/MarkupExtensions/I18nBinding.cs +++ b/src/Lang.Avalonia/MarkupExtensions/I18nBinding.cs @@ -1,53 +1,78 @@ -using Avalonia.Data; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using Avalonia.Data; using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Lang.Avalonia.Converters; -using System.Collections.Generic; namespace Lang.Avalonia.MarkupExtensions; +[EditorBrowsable(EditorBrowsableState.Never)] public class I18nBinding : MultiBindingExtensionBase { public I18nBinding(object key) { Mode = BindingMode.OneWay; - Converter = new I18nConverter(this); + Converter = new I18nConverter(); + ConverterParameter = this; KeyConverter = new I18nKeyConverter(); - ValueConverter = new I18nValueConverter(); - Args = new ArgCollection(this); + Key = key; + + var cultureBinding = new CompiledBindingExtension + { + Source = I18nManager.Instance, + Mode = BindingMode.OneWay, + Path = new CompiledBindingPathBuilder() + .Property( + new ClrPropertyInfo( + nameof(I18nManager.Culture), + target => ((I18nManager) target).Culture, + (target, value) => ((I18nManager) target).Culture = new CultureInfo((string) (value ?? throw new ArgumentNullException(nameof(value)))), + typeof(CultureInfo)), + PropertyInfoAccessorFactory.CreateInpcPropertyAccessor) + .Build() + }; - var cultureBinding = new Binding { Source = I18nManager.Instance, Path = nameof(I18nManager.Culture) }; Bindings.Add(cultureBinding); - Key = key; - if (Key is not BindingBase keyBinding) - { - keyBinding = new Binding { Source = key }; - } + if (key is not BindingBase keyBinding) + keyBinding = new CompiledBindingExtension { Source = key }; Bindings.Add(keyBinding); } - public I18nBinding(object key, string? cultureName, List args) : this(key) + public I18nBinding(object key, string? cultureName, IReadOnlyCollection args) : this(key) { CultureName = cultureName; - if (args is not { Count: > 0 }) - { - return; - } - foreach (object arg in args) + foreach (var arg in args) { - Args.Add(arg); + if (arg is IBinding binding) + { + Indexes.Add((true, Bindings.Count)); + Bindings.Add(binding); + } + else + { + Indexes.Add((false, Args.Count)); + Args.Add(arg); + } } } + internal List<(bool IsBinding, int Index)> Indexes { get; } = []; + public object Key { get; } public string? CultureName { get; set; } - public ArgCollection Args { get; } + internal List Args { get; } = []; - public IValueConverter KeyConverter { get; set; } + public IValueConverter? KeyConverter { get; set; } - public IValueConverter ValueConverter { get; set; } -} \ No newline at end of file + public IValueConverter? ValueConverter { get; set; } +} diff --git a/src/Lang.Avalonia/MarkupExtensions/I18nExtension.cs b/src/Lang.Avalonia/MarkupExtensions/I18nExtension.cs index 6a58f93..9708ad9 100644 --- a/src/Lang.Avalonia/MarkupExtensions/I18nExtension.cs +++ b/src/Lang.Avalonia/MarkupExtensions/I18nExtension.cs @@ -1,89 +1,64 @@ -using Avalonia.Markup.Xaml; using System; using System.Collections.Generic; +using Avalonia.Markup.Xaml; namespace Lang.Avalonia.MarkupExtensions; -public class I18nExtension : MarkupExtension +public class I18nExtension(object key) : MarkupExtension { - public I18nExtension(object key) - { - Key = key; - } - - public I18nExtension(object key, object arg0) : this(key) + public I18nExtension(object key, object arg0) + : this(key) { Args.Add(arg0); } - public I18nExtension(object key, object arg0, object arg1) : this(key) + public I18nExtension(object key, object arg0, object arg1) + : this(key, arg0) { - Args.Add(arg0); Args.Add(arg1); } - public I18nExtension(object key, object arg0, object arg1, object arg2) : this(key) + public I18nExtension(object key, object arg0, object arg1, object arg2) + : this(key, arg0, arg1) { - Args.Add(arg0); - Args.Add(arg1); Args.Add(arg2); } - public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3) : this(key) + public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3) + : this(key, arg0, arg1, arg2) { - Args.Add(arg0); - Args.Add(arg1); - Args.Add(arg2); Args.Add(arg3); } - public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4) : this(key) + public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4) + : this(key, arg0, arg1, arg2, arg3) { - Args.Add(arg0); - Args.Add(arg1); - Args.Add(arg2); - Args.Add(arg3); Args.Add(arg4); } public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4, object arg5) : - this(key) + this(key, arg0, arg1, arg2, arg3, arg4) { - Args.Add(arg0); - Args.Add(arg1); - Args.Add(arg2); - Args.Add(arg3); - Args.Add(arg4); Args.Add(arg5); } - public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4, object arg5, - object arg6) : this(key) + public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6) + : this(key, arg0, arg1, arg2, arg3, arg4, arg5) { - Args.Add(arg0); - Args.Add(arg1); - Args.Add(arg2); - Args.Add(arg3); - Args.Add(arg4); - Args.Add(arg5); Args.Add(arg6); } - public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4, object arg5, - object arg6, object arg7) : this(key) + public I18nExtension(object key, object arg0, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7) + : this(key, arg0, arg1, arg2, arg3, arg4, arg5, arg6) { - Args.Add(arg0); - Args.Add(arg1); - Args.Add(arg2); - Args.Add(arg3); - Args.Add(arg4); - Args.Add(arg5); - Args.Add(arg6); Args.Add(arg7); } - public object Key { get; } - public List Args { get; } = new(); + public object Key { get; } = key; + + public List Args { get; } = []; + public string? CultureName { get; set; } + public override object ProvideValue(IServiceProvider serviceProvider) => new I18nBinding(Key, CultureName, Args); } diff --git a/src/Lang.Avalonia/MarkupExtensions/MultiBindingExtensionBase.cs b/src/Lang.Avalonia/MarkupExtensions/MultiBindingExtensionBase.cs index cb6e8ef..a573641 100644 --- a/src/Lang.Avalonia/MarkupExtensions/MultiBindingExtensionBase.cs +++ b/src/Lang.Avalonia/MarkupExtensions/MultiBindingExtensionBase.cs @@ -1,4 +1,4 @@ -using Avalonia.Data; +using Avalonia.Data; using Avalonia.Data.Converters; using System; @@ -11,12 +11,10 @@ public abstract class MultiBindingExtensionBase : MultiBinding get => base.Converter; protected set { - if (base.Converter != null) - { + if (base.Converter is null) + base.Converter = value; + else throw new InvalidOperationException($"The {GetType().Name}.Converter property is readonly."); - } - - base.Converter = value; } } }