diff --git a/.editorconfig b/.editorconfig
index 928b3ce..ef55d07 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,23 +3,11 @@ root = true
# All files
[*]
indent_style = space
-end_of_line = crlf
+end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
-# C# files
-[*.cs]
-indent_size = 4
-csharp_new_line_before_open_brace = all
-csharp_prefer_braces = true:warning
-csharp_style_var_for_built_in_types = true:suggestion
-csharp_style_var_when_type_is_apparent = true:suggestion
-
-# AXAML files
-[*.axaml]
-indent_size = 2
-
# Xml files
[*.xml]
indent_size = 2
diff --git a/Settings.XamlStyler b/Settings.XamlStyler
new file mode 100644
index 0000000..8dcd894
--- /dev/null
+++ b/Settings.XamlStyler
@@ -0,0 +1,47 @@
+{
+ "IndentSize": 4,
+ "IndentWithTabs": null,
+ "AttributesTolerance": 5,
+ "KeepFirstAttributeOnSameLine": false,
+ "MaxAttributeCharactersPerLine": 80,
+ "MaxAttributesPerLine": 0,
+ "NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransform, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter",
+ "SeparateByGroups": true,
+ "AttributeIndentation": 0,
+ "AttributeIndentationStyle": "Spaces",
+ "RemoveDesignTimeReferences": false,
+ "EnableAttributeReordering": true,
+ "AttributeOrderingRuleGroups": [
+ "x:Class",
+ "xmlns, xmlns:x",
+ "xmlns:*",
+ "x:Key, Key, x:Name, Name, x:Uid, Uid, Title",
+ "Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom",
+ "Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight",
+ "Classes, Theme, Styles",
+ "Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex",
+ "*:*, *",
+ "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint",
+ "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText",
+ "Storyboard.*, From, To, Duration"
+ ],
+ "FirstLineAttributes": "",
+ "OrderAttributesByName": true,
+ "IgnoreDesignTimeReferencePrefix": false,
+ "PutEndingBracketOnNewLine": false,
+ "RemoveEndingTagOfEmptyElement": true,
+ "SpaceBeforeClosingSlash": false,
+ "RootElementLineBreakRule": "Default",
+ "ReorderVSM": "Last",
+ "ReorderGridChildren": false,
+ "ReorderCanvasChildren": false,
+ "ReorderSetters": "None",
+ "FormatMarkupExtension": false,
+ "NoNewLineMarkupExtensions": "x:Bind, Binding",
+ "ThicknessSeparator": "Comma",
+ "ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin",
+ "FormatOnSave": true,
+ "SaveAndCloseOnFormat": true,
+ "CommentPadding": 2,
+ "SuppressProcessing": false
+}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..3502a98
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "9.0.0",
+ "rollForward": "major"
+ }
+}
diff --git a/i18n/Aurora.I18N.Abstractions/Aurora.I18N.Abstractions.csproj b/i18n/Aurora.I18N.Abstractions/Aurora.I18N.Abstractions.csproj
new file mode 100644
index 0000000..20b2831
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/Aurora.I18N.Abstractions.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net9.0
+ enable
+ enable
+ IDE0130
+
+
+
diff --git a/i18n/Aurora.I18N.Abstractions/CultureChangedEventArgs.cs b/i18n/Aurora.I18N.Abstractions/CultureChangedEventArgs.cs
new file mode 100644
index 0000000..62b3b22
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/CultureChangedEventArgs.cs
@@ -0,0 +1,15 @@
+using System.Globalization;
+
+namespace Aurora.I18N;
+
+///
+/// 文化变更事件参数
+///
+public sealed class CultureChangedEventArgs(CultureInfo oldCulture, CultureInfo newCulture) : EventArgs
+{
+ public CultureInfo OldCulture { get; }
+ = oldCulture ?? throw new ArgumentNullException(nameof(oldCulture));
+
+ public CultureInfo NewCulture { get; }
+ = newCulture ?? throw new ArgumentNullException(nameof(newCulture));
+}
diff --git a/i18n/Aurora.I18N.Abstractions/ICultureProvider.cs b/i18n/Aurora.I18N.Abstractions/ICultureProvider.cs
new file mode 100644
index 0000000..807b39f
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/ICultureProvider.cs
@@ -0,0 +1,24 @@
+using System.Globalization;
+
+namespace Aurora.I18N;
+
+public interface ICultureProvider
+{
+ ///
+ /// 当前用于数值、日期等格式化的文化
+ /// 通常对应
+ ///
+ CultureInfo CurrentCulture { get; }
+
+ ///
+ /// 当前用于 UI 文本的文化
+ /// 通常对应
+ ///
+ CultureInfo CurrentUICulture { get; }
+
+ ///
+ /// 当当前文化发生变化时触发,用于通知 UI 或其它监听方刷新
+ ///
+ event EventHandler? CultureChanged;
+}
+
diff --git a/i18n/Aurora.I18N.Abstractions/IPluralizer.cs b/i18n/Aurora.I18N.Abstractions/IPluralizer.cs
new file mode 100644
index 0000000..87131c4
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/IPluralizer.cs
@@ -0,0 +1,6 @@
+namespace Aurora.I18N;
+
+///
+/// 关于
+///
+public interface IPluralizer;
diff --git a/i18n/Aurora.I18N.Abstractions/IResourceProvider.cs b/i18n/Aurora.I18N.Abstractions/IResourceProvider.cs
new file mode 100644
index 0000000..7e627d1
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/IResourceProvider.cs
@@ -0,0 +1,25 @@
+using System.Globalization;
+
+namespace Aurora.I18N;
+
+///
+/// 按 key 和文化提供本地化字符串的读取接口
+///
+public interface IResourceProvider
+{
+ ///
+ /// 按键名和文化获取本地化字符串,缺失时返回 null
+ ///
+ /// 资源键名
+ /// 目标文化信息
+ string? GetString(string key, CultureInfo culture);
+
+ ///
+ /// 获取指定文化下的全部键值对,可选包含父文化
+ ///
+ /// 目标文化信息
+ /// 是否同时包含父文化的资源
+ IEnumerable> GetAllStrings(
+ CultureInfo culture,
+ bool includeParentCultures);
+}
diff --git a/i18n/Aurora.I18N.Abstractions/ITextLocalizer.cs b/i18n/Aurora.I18N.Abstractions/ITextLocalizer.cs
new file mode 100644
index 0000000..6f95fcd
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/ITextLocalizer.cs
@@ -0,0 +1,46 @@
+namespace Aurora.I18N;
+
+///
+/// 泛型本地化接口,返回类型化结果并兼容基础本地化访问
+///
+/// 本地化结果的泛型类型
+public interface ITextLocalizer : ITextLocalizer { }
+
+
+///
+/// 定义文本本地化的统一访问接口,支持通过键名获取格式化后的字符串
+///
+public interface ITextLocalizer
+{
+ ///
+ /// 根据键名获取本地化字符串,未找到时通常返回键名自身
+ ///
+ /// 资源键名
+ string this[string key] { get; }
+
+ ///
+ /// 根据键名获取可格式化的本地化字符串,并应用提供的格式参数
+ ///
+ /// 资源键名
+ /// 格式化占位符对应的参数
+ string this[string key, params object[] arguments] { get; }
+
+ ///
+ /// 返回包含查找状态信息的本地化结果
+ ///
+ /// 资源键名
+ LocalizedString Get(string key);
+
+ ///
+ /// 返回格式化后的本地化结果,并附带查找状态信息
+ ///
+ /// 资源键名
+ /// 格式化占位符对应的参数
+ LocalizedString Get(string key, params object[] arguments);
+
+ ///
+ /// 获取当前文化(可选包含父文化)下的全部本地化字符串
+ ///
+ /// 是否同时包含父文化的资源
+ IEnumerable GetAllStrings(bool includeParentCultures);
+}
diff --git a/i18n/Aurora.I18N.Abstractions/LocalizedString.cs b/i18n/Aurora.I18N.Abstractions/LocalizedString.cs
new file mode 100644
index 0000000..eecd691
--- /dev/null
+++ b/i18n/Aurora.I18N.Abstractions/LocalizedString.cs
@@ -0,0 +1,40 @@
+namespace Aurora.I18N;
+
+///
+/// 表示一个本地化文本项,包含键名、值以及查找状态等信息。
+///
+/// 资源键名,通常用于回退显示或继续查询
+/// 本地化后的字符串,若查找失败则可能为 null
+/// 指示是否未找到对应资源
+/// 记录查找资源时检索过的位置,便于调试
+public readonly struct LocalizedString(
+ string name,
+ string? value,
+ bool resourceNotFound,
+ string? searchedLocation = null)
+{
+ ///
+ /// 本地化资源的键名
+ ///
+ public string Name { get; } = name;
+
+ ///
+ /// 匹配到的本地化文本,若未找到则保持为空
+ ///
+ public string? Value { get; } = value;
+
+ ///
+ /// 标记是否未找到对应资源,调用方可据此决定是否回退或记录日志
+ ///
+ public bool ResourceNotFound { get; } = resourceNotFound;
+
+ ///
+ /// 描述资源查找的来源或搜索路径,便于排查缺失问题
+ ///
+ public string? SearchedLocation { get; } = searchedLocation;
+
+ ///
+ /// 返回本地化值,如果缺失则回退到键名,保证调用方总能得到可显示内容
+ ///
+ public override string ToString() => Value ?? Name;
+}
diff --git a/i18n/i18n.slnx b/i18n/i18n.slnx
new file mode 100644
index 0000000..1c16724
--- /dev/null
+++ b/i18n/i18n.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/nuget.config b/nuget.config
index bc6e268..67a00ba 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,11 +1,12 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+