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_One_Module_Data_Constant_With_Compiled_BuiltIn_Bundles() { var execution = RunGenerator(); execution.Result.Diagnostics.ShouldBeEmpty(); var source = GetGeneratedSource(execution.Result, "GeneratedLanguageModuleRegistration.g.cs"); source.ShouldContain("builder.AddModuleData(\"Test.Package\", Data);"); var data = ReadModule(execution); var catalog = data.Catalogs.ShouldHaveSingleItem(); catalog.Units.Select(static unit => unit.Key).ShouldBe(["ItemCount", "Title"]); catalog.Units.Select(static unit => unit.IsFormatted).ShouldBe([true, false]); data.Bundles.Select(static bundle => bundle.Language).ShouldBe(["en-US", "zh-CN", "zh-TW"]); data.Bundles[1].Values.ShouldBe(["项目 {0}", "打开 \"文件\" C:\\临时"]); 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(2); 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 Encodes_Keyword_Unit_Identifiers_Without_Runtime_Discovery() { 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)); ReadModule(execution).Catalogs.ShouldHaveSingleItem().Units .Select(static unit => unit.Key).ShouldContain("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) { } } [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] public sealed class LanguageModuleManifestAttribute : System.Attribute { public LanguageModuleManifestAttribute(int version, string moduleId, string data) { } } public interface ILocalizationBuilder { void AddModuleData(string moduleId, string data); 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 static AtomUI.Localization.Data.LanguageModuleData ReadModule(TestGeneratorExecution execution) { var tree = execution.Result.GeneratedSources.Single(static source => source.HintName == "GeneratedLanguageModuleRegistration.g.cs").SyntaxTree; var value = tree.GetRoot(TestContext.Current.CancellationToken).DescendantNodes().OfType() .Single(static variable => variable.Identifier.ValueText == "Data").Initializer!.Value; var data = ((Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax)value).Token.ValueText; return AtomUI.Localization.Data.LanguageModuleDataCodec.Read("Test.Package", data); } }