diff --git a/.editorconfig b/.editorconfig index 1d122a2..f5a9cc0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,3 +9,7 @@ trim_trailing_whitespace = true [*.{yml,props,targets,csproj,slnx}] indent_size = 2 + +[*.cs] +# Use range operator +dotnet_diagnostic.IDE0057.severity = silent diff --git a/README.md b/README.md index bf58123..77b6322 100644 --- a/README.md +++ b/README.md @@ -2,74 +2,35 @@ [![NuGet](https://img.shields.io/nuget/v/Ramstack.LocaleAlignment.svg)](https://nuget.org/packages/Ramstack.LocaleAlignment) [![MIT](https://img.shields.io/github/license/rameel/ramstack.localealignment)](https://github.com/rameel/ramstack.localealignment/blob/main/LICENSE) -A Roslyn source generator that applies POSIX locale user overrides to .NET `CultureInfo` on Unix-like systems. +A small library for aligning .NET culture settings with POSIX locale user overrides on Unix-like systems. -![](https://github.com/rameel/ramstack.localealignment/blob/main/assets/screenshot.png) +![screenshot](https://raw.githubusercontent.com/rameel/ramstack.localealignment/c3abd3eb89b4b88965dede9f4a6b1eccb1b74b21/assets/screenshot.png) ## Problem -On Unix platforms, .NET determines the process culture using a limited subset of locale-related environment variables, -in the following order: -* `LC_ALL` -* `LC_MESSAGES` -* `LANG` +On Unix-like systems, .NET determines the process culture using only a subset of locale-related environment variables (`LC_ALL`, `LC_MESSAGES`, `LANG`). -Overrides such as `LC_NUMERIC`, `LC_TIME`, and `LC_MONETARY` are **ignored** for `CultureInfo` initialization, -even though they are explicitly defined by POSIX and commonly used by users to fine-tune locale behavior. - -As a result, applications may observe: - -* Incorrect numeric formatting when `LC_NUMERIC` is overridden -* Incorrect date and time formatting when `LC_TIME` is overridden -* Incorrect currency formatting when `LC_MONETARY` is overridden +Category-specific overrides such as `LC_NUMERIC`, `LC_TIME`, and `LC_MONETARY` **are not applied** during `CultureInfo` initialization. This behavior is documented and discussed in the .NET runtime repository: - * [https://github.com/dotnet/runtime/issues/110095](https://github.com/dotnet/runtime/issues/110095) -## What this package does -`Ramstack.LocaleAlignment` provides a **source generator** that injects a module initializer into your application. - -At startup, it: -* Detects whether the application is running on a non-Windows platform -* Respects .NET globalization invariant mode -* Reads the following environment variables independently: - * `LC_NUMERIC` - * `LC_MONETARY` - * `LC_TIME` -* Applies their effects to `CultureInfo` in a POSIX-consistent manner -The generator creates a derived `CultureInfo` instance that: -* Uses date/time formatting from `LC_TIME` -* Uses numeric formatting from `LC_NUMERIC` -* Uses currency formatting from `LC_MONETARY` +## Project structure +This repository contains two packages: -The resulting culture is applied to: +### Ramstack.LocaleAlignment +A small runtime library that explicitly applies POSIX locale category overrides to .NET `CultureInfo`. -* `CultureInfo.CurrentCulture` -* `CultureInfo.CurrentUICulture` -* `CultureInfo.DefaultThreadCurrentCulture` -* `CultureInfo.DefaultThreadCurrentUICulture` +It is intended to be called manually during application startup and can be used from any .NET language (`C#`, `F#`, `Nemerle`, etc.). -All initialization is performed defensively and never throws. +### Ramstack.LocaleAlignment.Generator +A dependency-free Roslyn source generator that enables automatic locale alignment at application startup. -## Usage -Add the package to your project: +The generator injects the equivalent of a `LocaleAlignment.Apply()` call directly into the compiled output, +without introducing any runtime dependency or requiring any configuration. -```bash -dotnet add package Ramstack.LocaleAlignment -``` - -No additional configuration or code changes are required. - -The generated initializer runs automatically at module load time. - -## Platform behavior -* **Windows**: no effect (not required - Windows automatically applies all user locale overrides) -* **Unix / Linux**: locale alignment is applied -* **Invariant globalization mode**: no effect ## Supported versions - | | Version | |------|----------------| | .NET | 6, 7, 8, 9, 10 | diff --git a/Ramstack.LocaleAlignment.slnx b/Ramstack.LocaleAlignment.slnx index f702abb..433db53 100644 --- a/Ramstack.LocaleAlignment.slnx +++ b/Ramstack.LocaleAlignment.slnx @@ -7,6 +7,7 @@ + diff --git a/src/Ramstack.LocaleAlignment/LocaleAlignmentGenerator.cs b/src/Ramstack.LocaleAlignment.Generator/LocaleAlignmentGenerator.cs similarity index 80% rename from src/Ramstack.LocaleAlignment/LocaleAlignmentGenerator.cs rename to src/Ramstack.LocaleAlignment.Generator/LocaleAlignmentGenerator.cs index 735d4ee..786a26c 100644 --- a/src/Ramstack.LocaleAlignment/LocaleAlignmentGenerator.cs +++ b/src/Ramstack.LocaleAlignment.Generator/LocaleAlignmentGenerator.cs @@ -1,36 +1,42 @@ using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; -namespace Ramstack.LocaleAlignment; +namespace Ramstack.LocaleAlignment.Generator; [Generator] -file sealed class LocaleAlignmentGenerator : IIncrementalGenerator +public sealed class LocaleAlignmentGenerator : IIncrementalGenerator { /// - public void Initialize(IncrementalGeneratorInitializationContext context) => - context.RegisterPostInitializationOutput(GenerateLocaleInitializer); + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var version = context.CompilationProvider.Select((c, _) => c is CSharpCompilation csharp ? (LanguageVersion?)csharp.LanguageVersion : null); + context.RegisterSourceOutput(version, GenerateLocaleInitializer); + } - private static void GenerateLocaleInitializer(IncrementalGeneratorPostInitializationContext context) + private static void GenerateLocaleInitializer(SourceProductionContext context, LanguageVersion? version) { - const string Source = """ - // + if (version is null) + return; + + var modifier = version == LanguageVersion.CSharp10 ? "internal" : "file"; - using System; - using System.Globalization; - using System.Runtime.CompilerServices; + var source = $$""" + // #nullable enable - file static class CultureInfoInitializer + [EditorBrowsable(EditorBrowsableState.Never)] + {{modifier}} static class LocaleAlignment { [ModuleInitializer] - public static void Initialize() + public static void Apply() { try { - InitializeCore(); + ApplyCore(); } catch { @@ -38,7 +44,7 @@ public static void Initialize() } } - private static void InitializeCore() + private static void ApplyCore() { if (!OperatingSystem.IsWindows() && !IsGlobalizationInvariant()) { @@ -150,28 +156,25 @@ static string ToBcp47(string posix) if (!modifier.IsEmpty) { - modifier = modifier switch - { - // Scripts - "arabic" => "Arab", - "cyrillic" => "Cyrl", - "devanagari" => "Deva", - "latin" => "Latn", - // "adlam" => "Adlm", - // "bengali" => "Beng", - // "gurmukhi" => "Guru", - // "olchiki" => "Olck", - // "orya" or "odia" => "Orya", - // "telugu" => "Telu", - // "tifinagh" => "Tfng", - // "vai" => "Vaii", - - // Variants - "valencia" => "valencia", - - // Skip - _ => "" - }; + // Scripts + if (modifier.SequenceEqual("arabic")) modifier = "Arab"; + else if (modifier.SequenceEqual("cyrillic")) modifier = "Cyrl"; + else if (modifier.SequenceEqual("devanagari")) modifier = "Deva"; + else if (modifier.SequenceEqual("latin")) modifier = "Latn"; + // else if (modifier.SequenceEqual("adlam")) modifier = "Adlm"; + // else if (modifier.SequenceEqual("bengali")) modifier = "Beng"; + // else if (modifier.SequenceEqual("gurmukhi")) modifier = "Guru"; + // else if (modifier.SequenceEqual("olchiki")) modifier = "Olck"; + // else if (modifier.SequenceEqual("orya")) modifier = "odia" => "Orya"; + // else if (modifier.SequenceEqual("telugu")) modifier = "Telu"; + // else if (modifier.SequenceEqual("tifinagh")) modifier = "Tfng"; + // else if (modifier.SequenceEqual("vai")) modifier = "Vaii"; + + // Variants + else if (modifier.SequenceEqual("valencia")) modifier = "valencia"; + // skip + else + modifier = default; } var buffer = posix.Length > 32 @@ -220,7 +223,6 @@ static string ToBcp47(string posix) buffer[i] = '-'; #endif - // create string posix = new string(buffer); return posix; } @@ -257,6 +259,6 @@ static bool IsGlobalizationInvariant() """; - context.AddSource("CultureInfoInitializer.g.cs", SourceText.From(Source, Encoding.UTF8)); + context.AddSource("LocaleAlignment.g.cs", SourceText.From(source, Encoding.UTF8)); } } diff --git a/src/Ramstack.LocaleAlignment.Generator/README.md b/src/Ramstack.LocaleAlignment.Generator/README.md new file mode 100644 index 0000000..680fdf1 --- /dev/null +++ b/src/Ramstack.LocaleAlignment.Generator/README.md @@ -0,0 +1,53 @@ +# Ramstack.LocaleAlignment.Generator +[![NuGet](https://img.shields.io/nuget/v/Ramstack.LocaleAlignment.Generator.svg)](https://nuget.org/packages/Ramstack.LocaleAlignment.Generator) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.localealignment)](https://github.com/rameel/ramstack.localealignment/blob/main/LICENSE) + +Utility that applies POSIX locale user overrides to .NET `CultureInfo` on Unix-like systems. + +![screenshot](https://raw.githubusercontent.com/rameel/ramstack.localealignment/c3abd3eb89b4b88965dede9f4a6b1eccb1b74b21/assets/screenshot.png) + +## Problem +On Unix-like systems, .NET determines the process culture using only a subset of locale-related environment variables (`LC_ALL`, `LC_MESSAGES`, `LANG`). + +Category-specific overrides such as `LC_NUMERIC`, `LC_TIME`, and `LC_MONETARY` **are not applied** during `CultureInfo` initialization. + +This behavior is documented and discussed in the .NET runtime repository: +* [https://github.com/dotnet/runtime/issues/110095](https://github.com/dotnet/runtime/issues/110095) + +## What this package does +`Ramstack.LocaleAlignment.Generator` provides a **source generator** that injects a module initializer into your application. + +At startup, it: +* Detects whether the application is running on a non-Windows platform +* Applies the following environment variables to `CultureInfo`: + * `LC_NUMERIC` + * `LC_MONETARY` + * `LC_TIME` +* Updates the current and default thread cultures +* Respects .NET globalization invariant mode + +Has no effect on Windows platforms. + +## Usage +Add the package to your project: + +```bash +dotnet add package Ramstack.LocaleAlignment.Generator +``` + +No additional configuration or code changes are required. + +The generated initializer runs automatically at module load time. + +## Supported versions + +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions +Bug reports and contributions are welcome. + +## License +This package is released as open source under the **MIT License**.
+See the [LICENSE](https://github.com/rameel/ramstack.localealignment/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.LocaleAlignment.Generator/Ramstack.LocaleAlignment.Generator.csproj b/src/Ramstack.LocaleAlignment.Generator/Ramstack.LocaleAlignment.Generator.csproj new file mode 100644 index 0000000..24d6d54 --- /dev/null +++ b/src/Ramstack.LocaleAlignment.Generator/Ramstack.LocaleAlignment.Generator.csproj @@ -0,0 +1,55 @@ + + + + netstandard2.0 + Library + Source generator that applies POSIX locale user overrides to .NET CultureInfo on Unix-like systems. + enable + true + latest + true + true + true + Analyzer + false + + + + Rameel + https://github.com/rameel/ramstack.localealignment + https://github.com/rameel/ramstack.localealignment#readme + MIT + README.md + ICU Unix Linux FreeBSD CultureInfo Locale POSIX + true + true + + + + $(NoWarn);NU5128 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + true + + + + + + + + + + + + diff --git a/src/Ramstack.LocaleAlignment/LocaleAlignment.cs b/src/Ramstack.LocaleAlignment/LocaleAlignment.cs new file mode 100644 index 0000000..94a889a --- /dev/null +++ b/src/Ramstack.LocaleAlignment/LocaleAlignment.cs @@ -0,0 +1,257 @@ +namespace Ramstack.LocaleAlignment; + +/// +/// Provides helpers for applying POSIX locale settings to .NET culture on Unix-like systems. +/// +/// +/// On Unix-like systems, .NET determines the process culture using only a subset +/// of locale-related environment variables (LC_ALL, LC_MESSAGES, LANG). +/// Category-specific overrides such as LC_NUMERIC, LC_TIME, and LC_MONETARY +/// are not applied during initialization. +/// +public static class LocaleAlignment +{ + private static int s_initialized; + + /// + /// Applies POSIX locale overrides (LC_NUMERIC, LC_TIME, LC_MONETARY). + /// + /// + /// + /// Call this method as early as possible during application startup, + /// before any culture-sensitive code runs. + /// + /// + /// It has no effect on Windows or in globalization-invariant mode. + /// + /// + public static void Apply() + { + try + { + if (Interlocked.Exchange(ref s_initialized, 1) == 0) + ApplyCore(); + } + catch + { + // Ignore errors + } + } + + private static void ApplyCore() + { + Console.WriteLine("ApplyCore"); + if (!OperatingSystem.IsWindows() && !IsGlobalizationInvariant()) + { + // + // LC_MONETARY must be processed after LC_NUMERIC to prevent LC_NUMERIC + // from overwriting currency-specific formatting overrides. + // + + CultureInfo? specific = null; + specific = TrySet("LC_NUMERIC", specific); + specific = TrySetMonetary(specific); + specific = TrySet("LC_TIME", specific); + + if (specific is not null) + { + CultureInfo.CurrentCulture = CultureInfo.ReadOnly(specific); + CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CurrentCulture; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CurrentCulture; + } + } + + static CultureInfo? TrySet(string variable, CultureInfo? specific) + { + var ci = GetCultureInfoFromEnvironment(variable); + if (ReferenceEquals(ci, CultureInfo.CurrentCulture)) + return specific; + + specific ??= (CultureInfo)CultureInfo.CurrentCulture.Clone(); + switch (variable) + { + case "LC_NUMERIC": + specific.NumberFormat = ci.NumberFormat; + break; + + case "LC_TIME": + specific.DateTimeFormat = ci.DateTimeFormat; + break; + } + + return specific; + } + + static CultureInfo? TrySetMonetary(CultureInfo? specific) + { + var monetary = GetCultureInfoFromEnvironment("LC_MONETARY"); + + if (!ReferenceEquals(monetary, CultureInfo.CurrentCulture)) + specific ??= (CultureInfo)CultureInfo.CurrentCulture.Clone(); + + // Restore currency from base culture if overwritten by LC_NUMERIC, + // or apply new currency settings if LC_MONETARY differs from base + if (specific is not null) + { + specific.NumberFormat = (NumberFormatInfo)specific.NumberFormat.Clone(); + specific.NumberFormat.CurrencySymbol = monetary.NumberFormat.CurrencySymbol; + specific.NumberFormat.CurrencyDecimalDigits = monetary.NumberFormat.CurrencyDecimalDigits; + specific.NumberFormat.CurrencyDecimalSeparator = monetary.NumberFormat.CurrencyDecimalSeparator; + specific.NumberFormat.CurrencyGroupSeparator = monetary.NumberFormat.CurrencyGroupSeparator; + specific.NumberFormat.CurrencyGroupSizes = monetary.NumberFormat.CurrencyGroupSizes; + specific.NumberFormat.CurrencyNegativePattern = monetary.NumberFormat.CurrencyNegativePattern; + specific.NumberFormat.CurrencyPositivePattern = monetary.NumberFormat.CurrencyPositivePattern; + } + + return specific; + } + + static CultureInfo GetCultureInfoFromEnvironment(string variable) + { + var name = Environment.GetEnvironmentVariable(variable)?.Trim(); + if (string.IsNullOrEmpty(name)) + return CultureInfo.CurrentCulture; + + name = ToBcp47(name); + if (string.Equals(name, CultureInfo.CurrentCulture.Name, StringComparison.OrdinalIgnoreCase)) + return CultureInfo.CurrentCulture; + + return CultureInfo.GetCultureInfo(name); + } + + static string ToBcp47(string posix) + { + // + // Converts a POSIX-style locale string (e.g., "ff_CM.UTF-8@latin") to + // a BCP-47 compliant culture name (e.g., "ff-Latn-CM"). + // + + if (posix.AsSpan().IndexOfAny('.', '_', '@') < 0) + return posix; + + // Format: + // language[_territory][.codeset][@modifier] + // + // en_IE.UTF-8@euro - full + // en_IE.UTF-8 - without modifier + // en_IE@euro - without codeset + // en_IE - minimal + + var di = posix.IndexOf('.'); + if (di < 0) + di = posix.Length; + + var ai = posix.IndexOf('@'); + if (ai < 0) + ai = posix.Length; + + var name = posix.AsSpan(0, Math.Min(di, ai)); + var modifier = posix.AsSpan(Math.Min(ai + 1, posix.Length)); + + if (!modifier.IsEmpty) + { + modifier = modifier switch + { + // Scripts + "arabic" => "Arab", + "cyrillic" => "Cyrl", + "devanagari" => "Deva", + "latin" => "Latn", + // "adlam" => "Adlm", + // "bengali" => "Beng", + // "gurmukhi" => "Guru", + // "olchiki" => "Olck", + // "orya" or "odia" => "Orya", + // "telugu" => "Telu", + // "tifinagh" => "Tfng", + // "vai" => "Vaii", + + // Variants + "valencia" => "valencia", + + // Skip + _ => "" + }; + } + + var buffer = posix.Length > 32 + ? new char[posix.Length] + : stackalloc char[32]; + + if (modifier.IsEmpty) + { + name.TryCopyTo(buffer); + buffer = buffer.Slice(0, name.Length); + } + else + { + // + // language-modifier-territory + // + var index = name.IndexOf('_'); + if (index > 0 && modifier.Length == 4 && (uint)modifier[0] - 'A' <= 'Z' - 'A') + { + // SuppressMessage + name.Slice(0, index).TryCopyTo(buffer); + buffer[index] = '-'; + + modifier.TryCopyTo(buffer.Slice(index + 1)); + buffer[index + 1 + modifier.Length] = '-'; + + name.Slice(index + 1).TryCopyTo(buffer.Slice(index + 2 + modifier.Length)); + buffer = buffer.Slice(0, name.Length + modifier.Length + 1); + } + else + { + // + // language-modifier + // + name.TryCopyTo(buffer); + buffer[name.Length] = '-'; + modifier.TryCopyTo(buffer.Slice(name.Length + 1)); + buffer = buffer.Slice(0, name.Length + 1 + modifier.Length); + } + } + + #if NET8_0_OR_GREATER + buffer.Replace('_', '-'); + #else + for (var i = 0; i < buffer.Length; i++) + if (buffer[i] == '_') + buffer[i] = '-'; + #endif + + posix = new string(buffer); + return posix; + } + + static bool IsGlobalizationInvariant() + { + // https://github.com/dotnet/runtime/blob/8fafcb3251a3d2ccde36612a2eab47e9ae8ebe2b/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.cs + + if (AppContext.TryGetSwitch("System.Globalization.Invariant", out var isInvariant) && isInvariant) + return true; + + var value = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); + if (value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)) + return true; + + try + { + // Starting in .NET 6, creating or resolving cultures other than the invariant one + // throws an exception except when "PredefinedCulturesOnly" is set to false + // (in this case, NativeName contains "Invariant" for all returned cultures). + // + // https://learn.microsoft.com/en-us/dotnet/core/compatibility/globalization/6.0/culture-creation-invariant-mode + // https://learn.microsoft.com/en-us/dotnet/core/runtime-config/globalization#predefined-cultures + + return CultureInfo.GetCultureInfo("en-US").NativeName.Contains("Invariant", StringComparison.Ordinal); + } + catch (CultureNotFoundException) + { + return true; + } + } + } +} diff --git a/src/Ramstack.LocaleAlignment/README.md b/src/Ramstack.LocaleAlignment/README.md new file mode 100644 index 0000000..ba3e0c0 --- /dev/null +++ b/src/Ramstack.LocaleAlignment/README.md @@ -0,0 +1,63 @@ +# Ramstack.LocaleAlignment +[![NuGet](https://img.shields.io/nuget/v/Ramstack.LocaleAlignment.svg)](https://nuget.org/packages/Ramstack.LocaleAlignment) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.localealignment)](https://github.com/rameel/ramstack.localealignment/blob/main/LICENSE) + +A small runtime helper for applying POSIX locale user overrides to .NET `CultureInfo` on Unix-like systems. + +![screenshot](https://raw.githubusercontent.com/rameel/ramstack.localealignment/c3abd3eb89b4b88965dede9f4a6b1eccb1b74b21/assets/screenshot.png) + +## Problem +On Unix-like systems, .NET determines the process culture using only a subset of locale-related environment variables (`LC_ALL`, `LC_MESSAGES`, `LANG`). + +Category-specific overrides such as `LC_NUMERIC`, `LC_TIME`, and `LC_MONETARY` **are not applied** during `CultureInfo` initialization. + +This behavior is documented and discussed in the .NET runtime repository: +* [https://github.com/dotnet/runtime/issues/110095](https://github.com/dotnet/runtime/issues/110095) + +## What this package does +`Ramstack.LocaleAlignment` provides a small runtime API that explicitly aligns +.NET culture settings with POSIX locale category overrides. + +When invoked at application startup, it: +* Detects whether the application is running on a Unix-like platform +* Applies the following environment variables to `CultureInfo`: + * `LC_NUMERIC` + * `LC_MONETARY` + * `LC_TIME` +* Updates the current and default thread cultures +* Respects .NET globalization invariant mode + +Has no effect on Windows platforms. + + +## Usage +Add the package to your project: + +```bash +dotnet add package Ramstack.LocaleAlignment +``` + +Call the alignment method as early as possible during application startup: +```csharp +LocaleAlignment.Apply(); +``` + +> [!NOTE] +> This package requires an explicit call. +> If you prefer automatic initialization via source generation, +> see [Ramstack.LocaleAlignment.Generator](https://www.nuget.org/packages/Ramstack.LocaleAlignment.Generator). + +No additional configuration or code changes are required. + + +## Supported versions +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions +Bug reports and contributions are welcome. + +## License +This package is released as open source under the **MIT License**.
+See the [LICENSE](https://github.com/rameel/ramstack.localealignment/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.LocaleAlignment/Ramstack.LocaleAlignment.csproj b/src/Ramstack.LocaleAlignment/Ramstack.LocaleAlignment.csproj index 5425070..92a6623 100644 --- a/src/Ramstack.LocaleAlignment/Ramstack.LocaleAlignment.csproj +++ b/src/Ramstack.LocaleAlignment/Ramstack.LocaleAlignment.csproj @@ -1,17 +1,12 @@  - netstandard2.0 - Source generator that applies POSIX locale user overrides to .NET CultureInfo on Unix-like systems. + net6.0 + Applies POSIX locale user overrides to .NET CultureInfo on Unix-like systems. enable true - latest + preview true - true - true - Analyzer - true - analyzers/dotnet/cs @@ -20,21 +15,20 @@ https://github.com/rameel/ramstack.localealignment#readme MIT README.md - ICU, Unix, Linux, FreeBSD, CultureInfo, POSIX + ICU Unix Linux FreeBSD CultureInfo Locale POSIX true true true snupkg true + true - - $(NoWarn);NU5128 - + + + - - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -42,10 +36,7 @@ - - true - \ - +