using System; using System.Collections.Generic; using System.Globalization; using System.Windows; using Humanizer; using PCL.Core.App.IoC; using PCL.Core.IO; namespace PCL.Core.App.Localization; /// ///

/// 本地化文本与展示格式访问辅助。 ///

///

/// 用于代码中读取本地化文本资源,以及按照当前展示区域性格式化文本参数、日期时间和数值。 /// 它面向 C# 代码侧调用;XAML 中的静态文本优先使用 DynamicResource, /// XAML 绑定值的格式化优先使用 。 ///

///

/// 文本资源来自当前应用的资源字典。正常运行时优先通过 /// 查找资源;在应用生命周期早期或测试环境中, /// 会回退到 进行一次安全查找。 ///

///

/// 该类中的格式化方法使用 , /// 因此会跟随 当前设置的展示格式区域性。 /// 它们只适合生成展示给用户看的文本,不应用于配置文件、日志、协议、缓存键、文件名等 /// 需要稳定格式的场景;这些场景应显式使用 。 ///

///
public static class Lang { private static readonly HashSet _UnrestrictedTimeZones = new(StringComparer.OrdinalIgnoreCase) { "China Standard Time" }; private static readonly HashSet _UnrestrictedCultures = new(StringComparer.OrdinalIgnoreCase) { "zh-CN" }; /// /// 当前代码侧本地化格式化使用的展示区域性。 /// public static CultureInfo Culture { get; private set; } = CultureInfo.CurrentCulture; /// /// 当前功能是否不受限制。 /// public static bool IsFeaturesUnrestricted => Config.Debug.AllowRestrictedFeature || ( _UnrestrictedTimeZones.Contains(TimeZoneInfo.Local.Id) && (_UnrestrictedCultures.Contains(CultureInfo.CurrentCulture.Name) || _UnrestrictedCultures.Contains(CultureInfo.CurrentUICulture.Name)) ); /// /// 当前展示区域性 是否为 zh-CN。 /// public static bool IsChineseMainland => Culture.Name == "zh-CN"; /// /// 同步展示区域性。该方法由 在语言或展示格式变化时调用。 /// /// /// 新的展示区域性。 /// internal static void SyncCulture(CultureInfo culture) { Culture = culture; } /// /// 获取指定资源键对应的本地化文本。 /// /// /// 资源键。不能为空、空字符串或空白字符串。 /// /// /// 找到资源时返回本地化文本; /// 未找到资源时,调试构建返回 !key!,发布构建返回 本身。 /// /// /// 为空、空字符串或空白字符串。 /// public static string Text(string key) { ArgumentException.ThrowIfNullOrWhiteSpace(key); if (Application.Current?.TryFindResource(key) is string text) return text; if (_LifecycleSafeFindResource(key) is string fallbackText) return fallbackText; #if DEBUG return $"!{key}!"; #else return key; #endif } /// /// 获取指定资源键对应的本地化格式文本,并使用当前展示区域性格式化参数。 /// 资源文本应使用标准 .NET 复合格式字符串,例如 {0}{1:N2}。 /// 该方法适合代码中生成用户可见句子,例如提示、说明、状态文本。 /// /// /// 资源键。不能为空、空字符串或空白字符串。 /// /// /// 用于填充资源文本中格式占位符的参数。 /// /// /// 使用当前展示区域性格式化后的本地化文本。 /// public static string Text(string key, params object?[] args) { return string.Format(Culture, Text(key), args); } /// ///

使用当前展示区域性格式化日期时间。

///
/// /// 要格式化的日期时间。 /// /// /// 标准或自定义日期时间格式字符串,默认使用 G。 /// /// /// 使用当前展示区域性格式化后的日期时间文本。 /// public static string Date(DateTime value, string format = "G") { return value.ToString(format, Culture); } /// ///

使用当前展示区域性格式化日期时间。

///
/// /// 要格式化的日期时间。 /// /// /// 标准或自定义日期时间格式字符串,默认使用 G。 /// /// /// 使用当前展示区域性格式化后的日期时间文本。 /// public static string Date(DateTimeOffset value, string format = "G") { return value.ToString(format, Culture); } /// /// 使用当前展示区域性格式化时间间隔。 /// /// /// 要格式化的时间间隔。负值表示过去,正值表示未来。 /// /// /// 要显示的时间单位数量。例如值为 1 时仅显示最大的一项时间单位,值为 2 时显示两项时间单位。 /// /// /// 如果为 ,会根据 的正负为文本添加“过去”或“未来”语义; /// 如果为 ,仅返回格式化后的时间间隔本身。 /// /// /// 允许显示的最大时间单位。 /// /// /// 允许显示的最小时间单位。 /// /// /// 使用当前展示区域性格式化后的时间间隔文本;当 时, /// 返回值会包含相对于当前时间的过去或未来语义。 /// public static string TimeSpan( TimeSpan span, int precision = 2, bool addAffixes = true, TimeUnit maxUnit = TimeUnit.Year, TimeUnit minUnit = TimeUnit.Hour) { var isPast = span.TotalMilliseconds < 0; if (isPast) span = span.Negate(); var text = span.Humanize( precision, maxUnit: maxUnit, minUnit: minUnit, culture: Culture); return addAffixes ? Text(isPast ? "Common.Format.TimeSpan.Past" : "Common.Format.TimeSpan.Future", text) : text; } /// /// 使用当前展示区域性格式化数值。 /// /// /// 实现 的数值或可格式化类型。 /// /// /// 要格式化的值。 /// /// /// 标准或自定义格式字符串。为 时使用类型默认格式。 /// /// /// 使用当前展示区域性格式化后的文本。 /// public static string Number(T value, string? format = null) where T : IFormattable { return value.ToString(format, Culture); } /// /// 使用当前展示区域性格式化紧凑数值,例如下载量。 /// /// /// 要格式化的整数。 /// /// /// 按当前展示区域性格式化后的紧凑数值文本,例如 11M2 万。 /// public static string CompactNumber(long value) { var absValue = Math.Abs((double)value); var sign = value < 0 ? -1 : 1; // 使用 4 位进位 if (_IsEastAsianCulture(Culture.Name)) return absValue switch { > 1_000_000_000_000d => Text("Common.Format.Number.Digit3", Number(sign * absValue / 1_000_000_000_000d, "N2")), > 100_000_000d => Text("Common.Format.Number.Digit2", Number(sign * absValue / 100_000_000d, "N2")), > 100_000d => Text("Common.Format.Number.Digit1", Number(sign * Math.Round(absValue / 10_000d), "N0")), _ => Number(value, "N0") }; // 使用 3 位进位 return absValue switch { > 1_000_000_000d => Text("Common.Format.Number.Digit3", Number(sign * absValue / 1_000_000_000d, "N2")), > 1_000_000d => Text("Common.Format.Number.Digit2", Number(sign * absValue / 1_000_000d, "N2")), > 10_000d => Text("Common.Format.Number.Digit1", Number(sign * Math.Round(absValue / 1_000d), "N0")), _ => Number(value, "N0") }; } /// /// 使用当前展示区域性格式化文件大小,例如 1.28 MB。 /// /// 字节数 /// 起始单位索引,0 为 B /// 格式化后的文件大小文本。 public static string FileSize(long length, int startUnit = 0) { return ByteStream.GetReadableLength(length, startUnit, Culture); } private static bool _IsEastAsianCulture(string cultureName) { return cultureName.StartsWith("zh", StringComparison.OrdinalIgnoreCase) || cultureName is "ja-JP" or "ko-KR" or "lzh"; } private static object? _LifecycleSafeFindResource(string key) { try { return Lifecycle.CurrentApplication.TryFindResource(key); } catch (InvalidOperationException) { return null; } catch (NullReferenceException) { return null; } } }