using Microsoft.CodeAnalysis; using Shouldly; using Xunit; using static AtomUI.Generator.Tests.Localization.LocalizationGeneratorTestHost; namespace AtomUI.Generator.Tests.Localization; public class LanguageCatalogSourceGeneratorTests { [Fact] public void Emits_The_Existing_Shape_Markup_Extension() { var execution = RunGenerator(); execution.Result.Diagnostics.ShouldBeEmpty(); var source = GetGeneratedSource( execution.Result, "TestApp.Localization.LoginLangResourceKind.LanguageCatalog.g.cs"); source.ShouldBe(ExpectedCatalogSource + "\n"); } [Fact] public void Emits_Strongly_Typed_Catalog_And_Compiled_BuiltIn_Bundles() { var execution = RunGenerator(); execution.Result.Diagnostics.ShouldBeEmpty(); var source = GetGeneratedSource( execution.Result, "TestApp.Localization.LoginLangResourceKind.LanguageCatalogRegistration.g.cs"); source.ShouldBe(ExpectedCatalogRegistrationSource + "\n"); GetGeneratedSource(execution.Result, "GeneratedLanguageModuleRegistration.g.cs") .ShouldBe(ExpectedModuleSource + "\n"); source.ShouldNotContain("Assembly.GetTypes"); source.ShouldNotContain("Type.GetFields"); source.ShouldNotContain("Enum.GetNames"); source.ShouldNotContain("GetCustomAttributes"); source.ShouldNotContain("Activator.CreateInstance"); source.ShouldNotContain("System.Xml"); source.ShouldNotContain("System.IO.File"); } [Fact] public void Generated_Catalog_Sources_Compile_Without_Errors() { var execution = RunGenerator(); execution.Result.GeneratedSources.Length.ShouldBe(3); execution.Result.GeneratedSources.ShouldNotContain(source => source.HintName.Contains("LinkedLanguageCatalogRegistration", StringComparison.Ordinal)); execution.DriverDiagnostics.ShouldBeEmpty(); execution.OutputCompilation.GetDiagnostics(TestContext.Current.CancellationToken) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); } [Fact] public void Appends_Extension_When_The_Catalog_Name_Does_Not_End_In_Kind() { var execution = RunWithOutputCompilation( CatalogAndRuntimeSource.Replace("LoginLangResourceKind", "LoginMessages"), LanguageFile( "Localization/en-US.xlf", SourceXliff.Replace("LoginLangResourceKind", "LoginMessages")), LanguageFile( "Localization/zh-CN.xlf", TargetXliff("zh-CN", "标题", "项目 {0}") .Replace("LoginLangResourceKind", "LoginMessages"))); execution.Result.Diagnostics.ShouldBeEmpty(); GetGeneratedSource( execution.Result, "TestApp.Localization.LoginMessages.LanguageCatalog.g.cs") .ShouldContain("public sealed class LoginMessagesExtension"); } [Fact] public void Escapes_Keyword_Unit_Identifiers_In_The_Generated_Switch() { var source = CatalogAndRuntimeSource.Replace("Title", "@class"); var sourceXliff = SourceXliff.Replace("id=\"Title\"", "id=\"class\""); var targetXliff = TargetXliff("zh-CN", "类型", "项目 {0}") .Replace("id=\"Title\"", "id=\"class\""); var execution = RunWithOutputCompilation( source, LanguageFile("Localization/en-US.xlf", sourceXliff), LanguageFile("Localization/zh-CN.xlf", targetXliff)); var moduleSource = GetGeneratedSource( execution.Result, "TestApp.Localization.LoginLangResourceKind.LanguageCatalogRegistration.g.cs"); moduleSource.ShouldContain("LoginLangResourceKind.@class =>"); execution.OutputCompilation.GetDiagnostics(TestContext.Current.CancellationToken) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); } [Fact] public void Generates_Unique_Extensions_For_Same_Named_Nested_Catalogs() { var catalogNamespaceIndex = CatalogAndRuntimeSource.IndexOf( "namespace TestApp.Localization", StringComparison.Ordinal); var source = CatalogAndRuntimeSource.Substring(0, catalogNamespaceIndex) + """ namespace TestApp.Localization { public static class OuterA { [AtomUI.Localization.LanguageCatalog] public enum CommonLangResourceKind { Title } } public static class OuterB { [AtomUI.Localization.LanguageCatalog] public enum CommonLangResourceKind { Title } } } """; var execution = RunWithOutputCompilation( source, LanguageFile( "Localization/OuterA/en-US.xlf", NestedSourceXliff("OuterA")), LanguageFile( "Localization/OuterB/en-US.xlf", NestedSourceXliff("OuterB"))); execution.Result.Diagnostics.ShouldBeEmpty(); GetGeneratedSource( execution.Result, "TestApp.Localization.OuterA+CommonLangResourceKind.LanguageCatalog.g.cs") .ShouldContain("public sealed class OuterA_CommonLangResourceExtension"); GetGeneratedSource( execution.Result, "TestApp.Localization.OuterB+CommonLangResourceKind.LanguageCatalog.g.cs") .ShouldContain("public sealed class OuterB_CommonLangResourceExtension"); execution.OutputCompilation.GetDiagnostics(TestContext.Current.CancellationToken) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); } [Fact] public void Generates_Unique_Extensions_When_Nested_Type_Paths_Contain_Underscores() { var catalogNamespaceIndex = CatalogAndRuntimeSource.IndexOf( "namespace TestApp.Localization", StringComparison.Ordinal); var source = CatalogAndRuntimeSource.Substring(0, catalogNamespaceIndex) + """ namespace TestApp.Localization { public static class A { public static class B { [AtomUI.Localization.LanguageCatalog] public enum CommonLangResourceKind { Title } } } public static class A_B { [AtomUI.Localization.LanguageCatalog] public enum CommonLangResourceKind { Title } } } """; var execution = RunWithOutputCompilation( source, LanguageFile( "Localization/A-B/en-US.xlf", NestedSourceXliff("A+B")), LanguageFile( "Localization/A_B/en-US.xlf", NestedSourceXliff("A_B"))); execution.Result.Diagnostics.ShouldBeEmpty(); execution.OutputCompilation.GetDiagnostics(TestContext.Current.CancellationToken) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); } [Fact] public void Generates_Unique_Extensions_For_Top_Level_Underscore_And_Nested_Catalogs() { var catalogNamespaceIndex = CatalogAndRuntimeSource.IndexOf( "namespace TestApp.Localization", StringComparison.Ordinal); var source = CatalogAndRuntimeSource.Substring(0, catalogNamespaceIndex) + """ namespace TestApp.Localization { [AtomUI.Localization.LanguageCatalog] public enum Outer_CommonLangResourceKind { Title } public static class Outer { [AtomUI.Localization.LanguageCatalog] public enum CommonLangResourceKind { Title } } } """; var execution = RunWithOutputCompilation( source, LanguageFile( "Localization/Outer_Common/en-US.xlf", """ Title """), LanguageFile( "Localization/Outer/en-US.xlf", NestedSourceXliff("Outer"))); execution.Result.Diagnostics.ShouldBeEmpty(); GetGeneratedSource( execution.Result, "TestApp.Localization.Outer_CommonLangResourceKind.LanguageCatalog.g.cs") .ShouldContain("public sealed class Outer_005FCommonLangResourceExtension"); GetGeneratedSource( execution.Result, "TestApp.Localization.Outer+CommonLangResourceKind.LanguageCatalog.g.cs") .ShouldContain("public sealed class Outer_CommonLangResourceExtension"); execution.OutputCompilation.GetDiagnostics(TestContext.Current.CancellationToken) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); } private static TestGeneratorExecution RunGenerator() { return RunWithOutputCompilation( CatalogAndRuntimeSource, LanguageFile("Localization/en-US.xlf", SourceXliff), LanguageFile("Localization/zh-CN.xlf", TargetXliff("zh-CN", "打开 \"文件\" C:\\临时", "项目 {0}")), LanguageFile("Localization/zh-TW.xlf", TargetXliff("zh-TW", "開啟 \"檔案\" C:\\暫存", "項目 {0}"))); } private static string GetGeneratedSource(GeneratorRunResult result, string hintName) { return result.GeneratedSources .Single(source => source.HintName == hintName) .SourceText .ToString(); } private static TestAdditionalText LanguageFile(string path, string content) { return new TestAdditionalText( path, content, new Dictionary(StringComparer.Ordinal) { ["build_metadata.AdditionalFiles.AtomUILanguage"] = "true", ["build_metadata.AdditionalFiles.AtomUILanguageSourceKind"] = "ModuleBuiltIn", ["build_metadata.AdditionalFiles.AtomUILanguageSourceIdentity"] = "Test.Package" }); } private static string TargetXliff(string language, string title, string itemCount) { return $$""" Items {0}{{itemCount}} Open "file" C:\Temp{{title}} """; } private static string NestedSourceXliff(string containingType) { return $$""" Title """; } private const string CatalogAndRuntimeSource = """ #nullable enable namespace AtomUI.Localization { [System.AttributeUsage(System.AttributeTargets.Enum, AllowMultiple = false)] public sealed class LanguageCatalogAttribute : System.Attribute; public abstract class LanguageResourceExtension where TResourceKind : struct, System.Enum { protected LanguageResourceExtension() { } protected LanguageResourceExtension(TResourceKind kind) { } } public interface ILocalizationBuilder { void AddCatalog(LanguageCatalogDescriptor descriptor); void AddTranslationBundle(TranslationBundleDescriptor descriptor); } public abstract class LanguageCatalogDescriptor { } public sealed class LanguageCatalogDescriptor : LanguageCatalogDescriptor where TResourceKind : struct, System.Enum { public LanguageCatalogDescriptor( string catalogId, System.Collections.Generic.IReadOnlyList units, System.Func unitSlotResolver) { } } public sealed class LanguageCatalogUnitDescriptor { public LanguageCatalogUnitDescriptor(string key, bool isFormatted = false) { } } public readonly struct LanguageTag { public static LanguageTag Parse(string value) => default; } public enum TranslationSourceKind : byte { ModuleBuiltIn, StaticLanguagePack, ApplicationOverride } public sealed class TranslationBundleDescriptor { public TranslationBundleDescriptor( string catalogId, LanguageTag language, TranslationSourceKind sourceKind, string sourceIdentity, System.Collections.Generic.IReadOnlyList values) { } } } namespace TestApp.Localization { [AtomUI.Localization.LanguageCatalog] public enum LoginLangResourceKind { ItemCount, Title } } """; private const string SourceXliff = """ Items {0} Open "file" C:\Temp """; private const string ExpectedCatalogSource = """ // #nullable enable namespace TestApp.Localization { public sealed class LoginLangResourceExtension : global::AtomUI.Localization.LanguageResourceExtension { public LoginLangResourceExtension() { } public LoginLangResourceExtension( global::TestApp.Localization.LoginLangResourceKind kind) : base(kind) { } } } """; private const string ExpectedCatalogRegistrationSource = """ // #nullable enable namespace AtomUI.Generated.TestApp { internal static class GeneratedLanguageCatalog_82D78C442594E820 { internal static void Register(global::AtomUI.Localization.ILocalizationBuilder builder) { global::System.ArgumentNullException.ThrowIfNull(builder); builder.AddCatalog( new global::AtomUI.Localization.LanguageCatalogDescriptor( "Test.Package:TestApp.Localization.LoginLangResourceKind", new global::AtomUI.Localization.LanguageCatalogUnitDescriptor[] { new global::AtomUI.Localization.LanguageCatalogUnitDescriptor("ItemCount", true), new global::AtomUI.Localization.LanguageCatalogUnitDescriptor("Title", false), }, static kind => kind switch { global::TestApp.Localization.LoginLangResourceKind.ItemCount => 0, global::TestApp.Localization.LoginLangResourceKind.Title => 1, _ => -1, })); builder.AddTranslationBundle( new global::AtomUI.Localization.TranslationBundleDescriptor( "Test.Package:TestApp.Localization.LoginLangResourceKind", global::AtomUI.Localization.LanguageTag.Parse("en-US"), global::AtomUI.Localization.TranslationSourceKind.ModuleBuiltIn, "Test.Package", new string?[] { "Items {0}", "Open \"file\" C:\\Temp", })); builder.AddTranslationBundle( new global::AtomUI.Localization.TranslationBundleDescriptor( "Test.Package:TestApp.Localization.LoginLangResourceKind", global::AtomUI.Localization.LanguageTag.Parse("zh-CN"), global::AtomUI.Localization.TranslationSourceKind.ModuleBuiltIn, "Test.Package", new string?[] { "项目 {0}", "打开 \"文件\" C:\\临时", })); builder.AddTranslationBundle( new global::AtomUI.Localization.TranslationBundleDescriptor( "Test.Package:TestApp.Localization.LoginLangResourceKind", global::AtomUI.Localization.LanguageTag.Parse("zh-TW"), global::AtomUI.Localization.TranslationSourceKind.ModuleBuiltIn, "Test.Package", new string?[] { "項目 {0}", "開啟 \"檔案\" C:\\暫存", })); } } } """; private const string ExpectedModuleSource = """ // #nullable enable [assembly: global::System.Reflection.AssemblyMetadata("AtomUILanguageModuleId", "Test.Package")] namespace AtomUI.Generated.TestApp { internal static class GeneratedLanguageModuleRegistration { internal static void Register(global::AtomUI.Localization.ILocalizationBuilder builder) { global::System.ArgumentNullException.ThrowIfNull(builder); global::AtomUI.Generated.TestApp.GeneratedLanguageCatalog_82D78C442594E820.Register(builder); } } } """; }