Skip to content

Comments

Fix/icu to nls mapping#20

Merged
gimlichael merged 10 commits intomainfrom
fix/icu-to-nls-mapping
Feb 21, 2026
Merged

Fix/icu to nls mapping#20
gimlichael merged 10 commits intomainfrom
fix/icu-to-nls-mapping

Conversation

@gimlichael
Copy link
Member

@gimlichael gimlichael commented Feb 21, 2026

This pull request introduces improvements to handling ICU-only culture names in the globalization extension project. The main focus is on ensuring that surrogate resources for these cultures are included and mapped correctly, both in the build process and in test coverage. Additionally, code formatting and organization have been improved in several files.

Resource handling and mapping enhancements:

  • Added removal and inclusion of surrogate resource files for ICU-only culture names in the project file, ensuring these cultures are properly embedded as resources. (Codebelt.Extensions.Globalization.csproj)
  • Implemented a new method, WriteIcuNamedNlsAlternatives, in Program.cs to map ICU-only culture names to their nearest NLS equivalents and copy surrogate files accordingly, so ICU-based environments can resolve them from the same embedded resource set. (tooling/gse/Program.cs)

Testing improvements:

  • Added a new test, UseNationalLanguageSupport_EnsureAllIcuCulturesAreMapped, to verify that all ICU culture names are mapped and covered by surrogate resources, improving test coverage for culture support. (CultureInfoExtensionsTest.cs)

Code organization and formatting:

  • Improved code formatting and organization in both test and tooling files, including splitting long parameter lists and updating using statements for clarity. (CultureInfoExtensionsTest.cs, Program.cs) [1] [2] [3]

Summary by CodeRabbit

  • Tests

    • Added a test that verifies all ICU cultures are mapped and supported, reporting any missing mappings.
  • Chores

    • Adjusted packaging to embed additional globalization resource files.
    • Enhanced tooling to generate and materialize ICU-to-NLS surrogate resources and fallback alternatives for wider locale coverage.
  • Bug Fixes

    • Added a defensive check to surface a clear error when an expected embedded globalization resource is missing.

@gimlichael gimlichael self-assigned this Feb 21, 2026
Copilot AI review requested due to automatic review settings February 21, 2026 02:21
@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

Adds embedding of surrogate resource files to the project file, introduces a test that verifies all specific ICU cultures map without errors, updates tooling to generate/write per-culture surrogate .yml/.bin files and to materialize ICU-named surrogates from NLS equivalents, and adds a defensive null-check when loading embedded surrogates.

Changes

Cohort / File(s) Summary
Project Resource Embedding
src/Codebelt.Extensions.Globalization/Codebelt.Extensions.Globalization.csproj
Added ItemGroup entries moving Surrogates from non-embedded handling into mirrored None Remove and EmbeddedResource blocks to embed surrogate resource files into the assembly.
ICU Culture Validation Test
test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs
Added UseNationalLanguageSupport_EnsureAllIcuCulturesAreMapped() test iterating specific cultures, collecting failures, and asserting no ICU-to-NLS mapping failures; added System and System.Collections.Generic usings and minor formatting tweaks.
Surrogate Generation Tooling
tooling/gse/Program.cs
Refactored surrogate generation: introduced WriteSurrogate(CultureInfo), TryWriteNlsSurrogate(string), and WriteIcuNamedNlsAlternatives(); centralizes YAML+gzipped .bin writing and copies/creates ICU-named surrogate files from nearest NLS equivalents.
Runtime Loading Safety
src/Codebelt.Extensions.Globalization/CultureInfoExtensions.cs
Added defensive null-check after locating NLS surrogate resource and throw InvalidOperationException if the embedded surrogate is missing/invalid to avoid downstream decompression/parsing on null data.

Sequence Diagram

sequenceDiagram
    participant Tool as gse Tooling
    participant FS as File System
    participant Build as Build Process
    participant Test as Test Runner

    Tool->>Tool: Enumerate all specific cultures
    Tool->>Tool: For each culture call WriteSurrogate(CultureInfo)
    Tool->>FS: Write .yml and gzipped .bin surrogate files
    Tool->>Tool: Call WriteIcuNamedNlsAlternatives()
    Tool->>FS: TryWriteNlsSurrogate(NLS-name) or Copy NLS .yml/.bin to ICU-name
    FS->>Build: Surrogate files are present on disk
    Build->>Build: csproj embeds resources into assembly
    Test->>Build: Test runner loads assembly
    Test->>Test: UseNationalLanguageSupport_EnsureAllIcuCulturesAreMapped() validates mappings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • V8.4.0/migrate from cuemon #1 — Related main PR touching surrogates packaging, CultureInfoExtensions, and tooling/gse surrogate generation.
  • V10.0.0/launch #13 — Changes to UseNationalLanguageSupport and related ICU/NLS mapping/enrichment logic; strongly related to the new test and mapping fallback.

Poem

🐰 I hopped through YAML, bin, and byte,

I paired ICU names by moonlit night,
When NLS friends were near at hand,
I copied, wrote, and made them stand,
Now every culture wakes up bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/icu to nls mapping' directly addresses the main purpose of the PR: establishing proper mapping and resource handling for ICU-only culture names to their NLS equivalents.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/icu-to-nls-mapping

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request enhances the globalization extension library to support ICU-only culture names that don't have direct Windows NLS equivalents. It creates a mapping infrastructure that copies surrogate resource files from NLS culture names to their ICU counterparts, ensuring cross-platform compatibility.

Changes:

  • Introduced a mapping dictionary and file-copying mechanism to create surrogate resources for 40 ICU culture names that differ from Windows NLS conventions
  • Added test coverage to verify all ICU-specific cultures can be resolved using the UseNationalLanguageSupport() extension method
  • Updated the project file to include 40 new embedded resource files (both removed from None and added as EmbeddedResource)

Reviewed changes

Copilot reviewed 3 out of 43 changed files in this pull request and generated no comments.

File Description
tooling/gse/Program.cs Adds WriteIcuNamedNlsAlternatives method with a dictionary mapping 40 ICU culture names to NLS equivalents, copies surrogate files for both .bin and .yml formats, and includes code formatting improvements
test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs Adds comprehensive test to verify all ICU cultures are mapped, includes code formatting improvements for ternary operator indentation
src/Codebelt.Extensions.Globalization/Codebelt.Extensions.Globalization.csproj Removes 40 surrogate files from None and adds them as EmbeddedResource entries for ICU culture variants
40 binary surrogate files (bgc-in.bin through zh-hant-tw.bin) New binary resource files containing culture-specific formatting data for ICU culture names
Comments suppressed due to low confidence (2)

test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs:1306

  • The test contains an empty foreach loop at lines 1303-1306. This appears to be dead code that serves no purpose. The loop iterates over missingIcuNames but doesn't perform any action inside the loop body. This should either be removed or the intended logic should be implemented (such as logging the missing culture names to test output).
    test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs:1298
  • The exception variable is declared but never used. Consider either removing the variable declaration and using a catch block without a variable (catch (Exception)), or use the exception variable to provide more detailed test output (e.g., logging exception messages to help diagnose why certain cultures are failing).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
tooling/gse/Program.cs (1)

123-131: Silent skip when source surrogate is missing could mask upstream failures.

If the NLS surrogate .bin or .yml was never generated (e.g., World.GetCultures didn't return a culture), the copy is silently skipped and the missing ICU surrogate will only surface later when the test or runtime fails. A Console.WriteLine warning here would make tooling runs easier to debug.

Proposed fix
                if (File.Exists(sourceBin))
                {
                    File.Copy(sourceBin, Path.Combine(SurrogatesPath, $"{icuName.ToLowerInvariant()}.bin"), overwrite: true);
                }
+               else
+               {
+                   Console.WriteLine($"Warning: NLS source '{sourceBin}' not found for ICU name '{icuName}'.");
+               }

                if (File.Exists(sourceYml))
                {
                    File.Copy(sourceYml, Path.Combine(SurrogatesPathRaw, $"{icuName.ToLowerInvariant()}.yml"), overwrite: true);
                }
+               else
+               {
+                   Console.WriteLine($"Warning: NLS source '{sourceYml}' not found for ICU name '{icuName}'.");
+               }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tooling/gse/Program.cs` around lines 123 - 131, The code currently silently
skips copying ICU surrogate files when sourceBin or sourceYml don't exist;
update the block around File.Exists checks (the logic that uses sourceBin,
sourceYml, SurrogatesPath, SurrogatesPathRaw and icuName in Program.cs) to emit
a Console.WriteLine warning whenever a source file is missing (include the
expected source path and the target path in the message) before skipping the
File.Copy, so missing surrogates are visible during tooling runs.
test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs (1)

1291-1298: Unused variable sut and swallowed exception details.

var sut on line 1293 is assigned but never read — use a discard. Additionally, the caught exception is unused; consider logging it for diagnostic visibility or at least including it in the failure output.

Proposed fix
                try
                {
-                    var sut = culture.UseNationalLanguageSupport();
+                    _ = culture.UseNationalLanguageSupport();
                }
-                catch (Exception exception)
+                catch (Exception)
                {
                    missingIcuNames.Add(culture.Name);
                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs`
around lines 1291 - 1298, Replace the unused local and the swallowed exception:
call UseNationalLanguageSupport with a discard (e.g., "_" instead of "var sut")
and capture the caught exception details instead of ignoring them—append
exception.ToString() (or exception.Message) to the diagnostic output (for
example add to missingIcuNames or a new missingIcuDetails collection) or emit it
via TestContext.WriteLine inside the catch; this change should be made around
the try/catch that invokes UseNationalLanguageSupport and updates
missingIcuNames.
src/Codebelt.Extensions.Globalization/Codebelt.Extensions.Globalization.csproj (1)

12-1310: Consider using a wildcard glob to reduce maintenance burden.

Both the original and new blocks explicitly enumerate every .bin surrogate. A wildcard glob like <EmbeddedResource Include="Surrogates\*.bin" /> (plus a single <None Remove="Surrogates\*.bin" />) would automatically pick up new surrogates produced by the tooling without manual csproj edits each time a culture is added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Codebelt.Extensions.Globalization/Codebelt.Extensions.Globalization.csproj`
around lines 12 - 1310, Replace the long per-file lists with a single wildcard
pair so the project automatically picks up new surrogate files: remove the
repeated explicit <None Remove="Surrogates\*.bin" /> and <EmbeddedResource
Include="Surrogates\*.bin" /> only (i.e. replace the many <None
Remove="Surrogates\*.bin" /> entries with one <None Remove="Surrogates\*.bin" />
and replace the many <EmbeddedResource Include="Surrogates\*.bin" /> entries
with one <EmbeddedResource Include="Surrogates\*.bin" />), keeping the
<ItemGroup> wrapper(s) as needed so MSBuild still excludes then embeds all .bin
files produced by tooling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs`:
- Around line 1301-1307: The empty foreach over missingIcuNames in
CultureInfoExtensionsTest is dead code; either remove the entire if/foreach
block or, if intended for diagnostics, iterate missingIcuNames and call
TestOutput.WriteLine for each item (e.g., "Missing ICU name: {name}") so the
test output shows which names are missing; update the block around
missingIcuNames accordingly.

---

Nitpick comments:
In
`@src/Codebelt.Extensions.Globalization/Codebelt.Extensions.Globalization.csproj`:
- Around line 12-1310: Replace the long per-file lists with a single wildcard
pair so the project automatically picks up new surrogate files: remove the
repeated explicit <None Remove="Surrogates\*.bin" /> and <EmbeddedResource
Include="Surrogates\*.bin" /> only (i.e. replace the many <None
Remove="Surrogates\*.bin" /> entries with one <None Remove="Surrogates\*.bin" />
and replace the many <EmbeddedResource Include="Surrogates\*.bin" /> entries
with one <EmbeddedResource Include="Surrogates\*.bin" />), keeping the
<ItemGroup> wrapper(s) as needed so MSBuild still excludes then embeds all .bin
files produced by tooling.

In `@test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs`:
- Around line 1291-1298: Replace the unused local and the swallowed exception:
call UseNationalLanguageSupport with a discard (e.g., "_" instead of "var sut")
and capture the caught exception details instead of ignoring them—append
exception.ToString() (or exception.Message) to the diagnostic output (for
example add to missingIcuNames or a new missingIcuDetails collection) or emit it
via TestContext.WriteLine inside the catch; this change should be made around
the try/catch that invokes UseNationalLanguageSupport and updates
missingIcuNames.

In `@tooling/gse/Program.cs`:
- Around line 123-131: The code currently silently skips copying ICU surrogate
files when sourceBin or sourceYml don't exist; update the block around
File.Exists checks (the logic that uses sourceBin, sourceYml, SurrogatesPath,
SurrogatesPathRaw and icuName in Program.cs) to emit a Console.WriteLine warning
whenever a source file is missing (include the expected source path and the
target path in the message) before skipping the File.Copy, so missing surrogates
are visible during tooling runs.

Comment on lines +1301 to +1307
if (missingIcuNames.Count > 0)
{
foreach (var missingIcuName in missingIcuNames)
{

}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty foreach loop — dead code or incomplete implementation.

The if block on lines 1301-1307 iterates missingIcuNames but the loop body is empty. This looks like leftover scaffolding — likely intended to log each missing name via TestOutput.WriteLine.

Proposed fix: either remove the block or log the missing names
-            if (missingIcuNames.Count > 0)
-            {
-                foreach (var missingIcuName in missingIcuNames)
-                {
-
-                }
-            }
+            foreach (var missingIcuName in missingIcuNames)
+            {
+                TestOutput.WriteLine($"Missing ICU mapping: {missingIcuName}");
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Codebelt.Extensions.Globalization.Tests/CultureInfoExtensionsTest.cs`
around lines 1301 - 1307, The empty foreach over missingIcuNames in
CultureInfoExtensionsTest is dead code; either remove the entire if/foreach
block or, if intended for diagnostics, iterate missingIcuNames and call
TestOutput.WriteLine for each item (e.g., "Missing ICU name: {name}") so the
test output shows which names are missing; update the block around
missingIcuNames accordingly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tooling/gse/Program.cs (2)

176-194: Silent skip when source surrogate files are missing.

If sourceBin / sourceYml don't exist, the loop silently moves on. For a build-time code-generation tool, this can mask a misconfigured mapping (e.g., a typo in the NLS name, or the NLS surrogate never having been written in the earlier loop). A Console.WriteLine warning would surface these issues during generation without breaking the flow.

♻️ Suggested improvement
             if (File.Exists(sourceBin))
             {
                 File.Copy(sourceBin, Path.Combine(SurrogatesPath, $"{icuName.ToLowerInvariant()}.bin"), overwrite: true);
             }
+            else
+            {
+                Console.WriteLine($"Warning: NLS source '{sourceBin}' not found for ICU mapping '{icuName}'.");
+            }

             if (File.Exists(sourceYml))
             {
                 File.Copy(sourceYml, Path.Combine(SurrogatesPathRaw, $"{icuName.ToLowerInvariant()}.yml"), overwrite: true);
             }
+            else
+            {
+                Console.WriteLine($"Warning: NLS raw source '{sourceYml}' not found for ICU mapping '{icuName}'.");
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tooling/gse/Program.cs` around lines 176 - 194, The loop that copies mapped
surrogate files (inside the foreach over icuToNls) silently skips when sourceBin
or sourceYml are missing; update the logic in the foreach block (around
TryWriteNlsSurrogate, sourceBin, sourceYml and the File.Exists checks) to emit a
Console.WriteLine warning identifying the missing file(s) and the affected
mapping (icuName -> nlsName) whenever neither the .bin nor .yml source exists so
build-time misconfigurations are visible while keeping the existing copy
behavior otherwise.

43-66: ms stream from YamlFormatter.SerializeObject is never disposed.

While MemoryStream.Dispose() is largely a no-op in .NET, wrapping ms in a using statement keeps the method consistent with how fsRawYaml, cms, and fs are handled.

♻️ Suggested fix
 private static void WriteSurrogate(CultureInfo cultureInfo)
 {
     var dtSurrogate = new DateTimeFormatInfoSurrogate(cultureInfo.DateTimeFormat);
     var nfSurrogate = new NumberFormatInfoSurrogate(cultureInfo.NumberFormat);
     var ciSurrogate = new CultureInfoSurrogate(dtSurrogate, nfSurrogate);

-    var ms = YamlFormatter.SerializeObject(ciSurrogate, o =>
+    using var ms = YamlFormatter.SerializeObject(ciSurrogate, o =>
     {
         o.Settings.NamingConvention = NullNamingConvention.Instance;
         o.Settings.ReflectionRules = new MemberReflection();
         o.Settings.IndentSequences = false;
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tooling/gse/Program.cs` around lines 43 - 66, The MemoryStream returned by
YamlFormatter.SerializeObject (variable ms in WriteSurrogate) is not disposed;
change its declaration to a using declaration (e.g. using var ms =
YamlFormatter.SerializeObject(...)) so ms is disposed after you finish creating
fsRawYaml, cms and fs; keep the existing sequence (create ms, write raw YAML,
create cms = ms.CompressGZip(), write compressed blob) and preserve the
LeaveOpen behavior on the ToByteArray calls so disposing ms happens only after
you finish using it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tooling/gse/Program.cs`:
- Around line 176-194: The loop that copies mapped surrogate files (inside the
foreach over icuToNls) silently skips when sourceBin or sourceYml are missing;
update the logic in the foreach block (around TryWriteNlsSurrogate, sourceBin,
sourceYml and the File.Exists checks) to emit a Console.WriteLine warning
identifying the missing file(s) and the affected mapping (icuName -> nlsName)
whenever neither the .bin nor .yml source exists so build-time misconfigurations
are visible while keeping the existing copy behavior otherwise.
- Around line 43-66: The MemoryStream returned by YamlFormatter.SerializeObject
(variable ms in WriteSurrogate) is not disposed; change its declaration to a
using declaration (e.g. using var ms = YamlFormatter.SerializeObject(...)) so ms
is disposed after you finish creating fsRawYaml, cms and fs; keep the existing
sequence (create ms, write raw YAML, create cms = ms.CompressGZip(), write
compressed blob) and preserve the LeaveOpen behavior on the ToByteArray calls so
disposing ms happens only after you finish using it.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Codebelt.Extensions.Globalization/CultureInfoExtensions.cs (1)

29-31: 🛠️ Refactor suggestion | 🟠 Major

Document the new InvalidOperationException in the XML doc for both public overloads.

Neither overload's <exception> block covers the InvalidOperationException now thrown at line 63 when a culture has no embedded surrogate. The collection overload throws it directly; the single-culture overload inherits the same path via delegation.

📄 Proposed additions
 /// <exception cref="ArgumentNullException">
 /// <paramref name="culture"/> is null.
 /// </exception>
+/// <exception cref="InvalidOperationException">
+/// No NLS surrogate is available for <paramref name="culture"/>.
+/// </exception>
 public static CultureInfo UseNationalLanguageSupport(this CultureInfo culture)
 /// <exception cref="ArgumentNullException">
 /// <paramref name="cultures"/> is null.
 /// </exception>
+/// <exception cref="InvalidOperationException">
+/// No NLS surrogate is available for one of the elements in <paramref name="cultures"/>.
+/// </exception>
 public static IEnumerable<CultureInfo> UseNationalLanguageSupport(this IEnumerable<CultureInfo> cultures)

As per coding guidelines: "Add XML doc comments to public and protected classes and methods where appropriate."

Also applies to: 44-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Codebelt.Extensions.Globalization/CultureInfoExtensions.cs` around lines
29 - 31, Update the XML docs for the two public overloads in
CultureInfoExtensions (the single-culture overload taking CultureInfo and the
collection overload taking IEnumerable<CultureInfo> or similar) to include an
<exception cref="InvalidOperationException"> entry explaining it is thrown when
a culture (or any culture in the collection) has no embedded surrogate; ensure
both overloads’ <exception> blocks document this new InvalidOperationException
alongside the existing ArgumentNullException notes.
🧹 Nitpick comments (1)
tooling/gse/Program.cs (1)

55-62: Prefer bytes.Length over (int)stream.Length as the Write count argument.

Both ms.ToByteArray(...) and cms.ToByteArray(...) return the serialized/compressed data. Passing (int)ms.Length / (int)cms.Length as the count implicitly assumes those lengths match the returned array, which holds today because the Cuemon ToByteArray extension uses MemoryStream.ToArray() (returning exactly Length bytes) for MemoryStream instances. Using the returned array's own .Length makes the intent explicit and is more defensive if the stream type ever changes.

♻️ Proposed refactor
-            var rawYamlBytes = ms.ToByteArray(o => o.LeaveOpen = true);
-            fsRawYaml.Write(rawYamlBytes, 0, (int)ms.Length);
+            var rawYamlBytes = ms.ToByteArray(o => o.LeaveOpen = true);
+            fsRawYaml.Write(rawYamlBytes, 0, rawYamlBytes.Length);
             fsRawYaml.Flush();

             using var cms = ms.CompressGZip();
-            var compressedBytes = cms.ToByteArray(o => o.LeaveOpen = true);
-            fs.Write(compressedBytes, 0, (int)cms.Length);
+            var compressedBytes = cms.ToByteArray(o => o.LeaveOpen = true);
+            fs.Write(compressedBytes, 0, compressedBytes.Length);
             fs.Flush();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tooling/gse/Program.cs` around lines 55 - 62, The Write calls currently use
(int)ms.Length/(int)cms.Length which assumes the stream lengths match the
returned arrays; instead capture the byte arrays returned by ms.ToByteArray(...)
and cms.ToByteArray(...) (e.g., var raw = ms.ToByteArray(...); var compressed =
cms.ToByteArray(...)) and pass raw.Length and compressed.Length as the count to
fsRawYaml.Write and fs.Write respectively (use the returned arrays' Length to be
explicit and defensive when writing from fsRawYaml/fs), keeping the existing
LeaveOpen options when calling ToByteArray.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tooling/gse/Program.cs`:
- Around line 173-191: The loop over icuToNls silently skips copying when
TryWriteNlsSurrogate(icuName) returns false and the mapped files
(sourceBin/sourceYml under SurrogatesPath/SurrogatesPathRaw) are missing; add
Console.Error warnings when File.Exists(sourceBin) or File.Exists(sourceYml) is
false to surface stale/missing mappings at generation time. Specifically, in the
foreach handling of (icuName, nlsName) around TryWriteNlsSurrogate, after
computing sourceBin and sourceYml and before/after each File.Copy, emit a
descriptive Console.Error message including icuName and nlsName whenever a
required source file is not found so tooling users see which ICU cultures will
lack surrogates (this will help trace later InvalidOperationException from
UseNationalLanguageSupport).

---

Outside diff comments:
In `@src/Codebelt.Extensions.Globalization/CultureInfoExtensions.cs`:
- Around line 29-31: Update the XML docs for the two public overloads in
CultureInfoExtensions (the single-culture overload taking CultureInfo and the
collection overload taking IEnumerable<CultureInfo> or similar) to include an
<exception cref="InvalidOperationException"> entry explaining it is thrown when
a culture (or any culture in the collection) has no embedded surrogate; ensure
both overloads’ <exception> blocks document this new InvalidOperationException
alongside the existing ArgumentNullException notes.

---

Nitpick comments:
In `@tooling/gse/Program.cs`:
- Around line 55-62: The Write calls currently use
(int)ms.Length/(int)cms.Length which assumes the stream lengths match the
returned arrays; instead capture the byte arrays returned by ms.ToByteArray(...)
and cms.ToByteArray(...) (e.g., var raw = ms.ToByteArray(...); var compressed =
cms.ToByteArray(...)) and pass raw.Length and compressed.Length as the count to
fsRawYaml.Write and fs.Write respectively (use the returned arrays' Length to be
explicit and defensive when writing from fsRawYaml/fs), keeping the existing
LeaveOpen options when calling ToByteArray.

Comment on lines +173 to 191
foreach (var (icuName, nlsName) in icuToNls)
{
// Prefer a native NLS surrogate for the ICU name if Windows supports it;
// only fall back to copying the mapped NLS surrogate if it does not.
if (TryWriteNlsSurrogate(icuName)) continue;

var sourceBin = Path.Combine(SurrogatesPath, $"{nlsName.ToLowerInvariant()}.bin");
var sourceYml = Path.Combine(SurrogatesPathRaw, $"{nlsName.ToLowerInvariant()}.yml");

if (File.Exists(sourceBin))
{
File.Copy(sourceBin, Path.Combine(SurrogatesPath, $"{icuName.ToLowerInvariant()}.bin"), overwrite: true);
}

if (File.Exists(sourceYml))
{
var dtSurrogate = new DateTimeFormatInfoSurrogate(cultureInfo.DateTimeFormat);
var nfSurrogate = new NumberFormatInfoSurrogate(cultureInfo.NumberFormat);
var ciSurrogate = new CultureInfoSurrogate(dtSurrogate, nfSurrogate);

var ms = YamlFormatter.SerializeObject(ciSurrogate, o =>
{
o.Settings.NamingConvention = NullNamingConvention.Instance;
o.Settings.ReflectionRules = new MemberReflection();
o.Settings.IndentSequences = false;
});

using var fsRawYaml = new FileStream(Path.Combine(SurrogatesPathRaw, $"{cultureInfo.Name.ToLowerInvariant()}.yml"), FileMode.Create);
fsRawYaml.Write(ms.ToByteArray(o => o.LeaveOpen = true), 0, (int)ms.Length);
fsRawYaml.Flush();

using var cms = ms.CompressGZip();
using var fs = new FileStream(Path.Combine(SurrogatesPath, $"{cultureInfo.Name.ToLowerInvariant()}.bin"), FileMode.Create);
fs.Write(cms.ToByteArray(o => o.LeaveOpen = true), 0, (int)cms.Length);
fs.Flush();
File.Copy(sourceYml, Path.Combine(SurrogatesPathRaw, $"{icuName.ToLowerInvariant()}.yml"), overwrite: true);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent skip when source surrogate files are missing can produce an invisible mapping gap.

When TryWriteNlsSurrogate returns false and either sourceBin or sourceYml doesn't exist (e.g., the NLS name in the dictionary is stale or was never generated), the ICU culture is silently left without a surrogate. At runtime, UseNationalLanguageSupport will throw InvalidOperationException for that culture with no indication that the tooling already knew about the gap. Emitting at least a Console.Error warning when a copy is skipped would surface the problem at generation time.

🛡️ Proposed improvement
             if (File.Exists(sourceBin))
             {
                 File.Copy(sourceBin, Path.Combine(SurrogatesPath, $"{icuName.ToLowerInvariant()}.bin"), overwrite: true);
             }
+            else
+            {
+                Console.Error.WriteLine($"[WARN] No surrogate found for ICU culture '{icuName}': source '{sourceBin}' does not exist. This culture will have no embedded resource.");
+            }

             if (File.Exists(sourceYml))
             {
                 File.Copy(sourceYml, Path.Combine(SurrogatesPathRaw, $"{icuName.ToLowerInvariant()}.yml"), overwrite: true);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (var (icuName, nlsName) in icuToNls)
{
// Prefer a native NLS surrogate for the ICU name if Windows supports it;
// only fall back to copying the mapped NLS surrogate if it does not.
if (TryWriteNlsSurrogate(icuName)) continue;
var sourceBin = Path.Combine(SurrogatesPath, $"{nlsName.ToLowerInvariant()}.bin");
var sourceYml = Path.Combine(SurrogatesPathRaw, $"{nlsName.ToLowerInvariant()}.yml");
if (File.Exists(sourceBin))
{
File.Copy(sourceBin, Path.Combine(SurrogatesPath, $"{icuName.ToLowerInvariant()}.bin"), overwrite: true);
}
if (File.Exists(sourceYml))
{
var dtSurrogate = new DateTimeFormatInfoSurrogate(cultureInfo.DateTimeFormat);
var nfSurrogate = new NumberFormatInfoSurrogate(cultureInfo.NumberFormat);
var ciSurrogate = new CultureInfoSurrogate(dtSurrogate, nfSurrogate);
var ms = YamlFormatter.SerializeObject(ciSurrogate, o =>
{
o.Settings.NamingConvention = NullNamingConvention.Instance;
o.Settings.ReflectionRules = new MemberReflection();
o.Settings.IndentSequences = false;
});
using var fsRawYaml = new FileStream(Path.Combine(SurrogatesPathRaw, $"{cultureInfo.Name.ToLowerInvariant()}.yml"), FileMode.Create);
fsRawYaml.Write(ms.ToByteArray(o => o.LeaveOpen = true), 0, (int)ms.Length);
fsRawYaml.Flush();
using var cms = ms.CompressGZip();
using var fs = new FileStream(Path.Combine(SurrogatesPath, $"{cultureInfo.Name.ToLowerInvariant()}.bin"), FileMode.Create);
fs.Write(cms.ToByteArray(o => o.LeaveOpen = true), 0, (int)cms.Length);
fs.Flush();
File.Copy(sourceYml, Path.Combine(SurrogatesPathRaw, $"{icuName.ToLowerInvariant()}.yml"), overwrite: true);
}
}
foreach (var (icuName, nlsName) in icuToNls)
{
// Prefer a native NLS surrogate for the ICU name if Windows supports it;
// only fall back to copying the mapped NLS surrogate if it does not.
if (TryWriteNlsSurrogate(icuName)) continue;
var sourceBin = Path.Combine(SurrogatesPath, $"{nlsName.ToLowerInvariant()}.bin");
var sourceYml = Path.Combine(SurrogatesPathRaw, $"{nlsName.ToLowerInvariant()}.yml");
if (File.Exists(sourceBin))
{
File.Copy(sourceBin, Path.Combine(SurrogatesPath, $"{icuName.ToLowerInvariant()}.bin"), overwrite: true);
}
else
{
Console.Error.WriteLine($"[WARN] No surrogate found for ICU culture '{icuName}': source '{sourceBin}' does not exist. This culture will have no embedded resource.");
}
if (File.Exists(sourceYml))
{
File.Copy(sourceYml, Path.Combine(SurrogatesPathRaw, $"{icuName.ToLowerInvariant()}.yml"), overwrite: true);
}
else
{
Console.Error.WriteLine($"[WARN] No surrogate found for ICU culture '{icuName}': source '{sourceYml}' does not exist. This culture will have no embedded resource.");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tooling/gse/Program.cs` around lines 173 - 191, The loop over icuToNls
silently skips copying when TryWriteNlsSurrogate(icuName) returns false and the
mapped files (sourceBin/sourceYml under SurrogatesPath/SurrogatesPathRaw) are
missing; add Console.Error warnings when File.Exists(sourceBin) or
File.Exists(sourceYml) is false to surface stale/missing mappings at generation
time. Specifically, in the foreach handling of (icuName, nlsName) around
TryWriteNlsSurrogate, after computing sourceBin and sourceYml and before/after
each File.Copy, emit a descriptive Console.Error message including icuName and
nlsName whenever a required source file is not found so tooling users see which
ICU cultures will lack surrogates (this will help trace later
InvalidOperationException from UseNationalLanguageSupport).

@sonarqubecloud
Copy link

@gimlichael gimlichael merged commit d94e7da into main Feb 21, 2026
21 checks passed
@gimlichael gimlichael deleted the fix/icu-to-nls-mapping branch February 21, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant