Files
AtomUI/tests/AtomUI.Generator.Tests/Localization/LanguageCatalogSourceGeneratorTests.cs

488 lines
20 KiB
C#

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",
"""
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US">
<file id="TestApp.Localization.Outer_CommonLangResourceKind">
<unit id="Title"><segment><source>Title</source></segment></unit>
</file>
</xliff>
"""),
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<string, string>(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 $$"""
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US" trgLang="{{language}}">
<file id="TestApp.Localization.LoginLangResourceKind">
<unit id="ItemCount"><segment><source>Items {0}</source><target state="translated">{{itemCount}}</target></segment></unit>
<unit id="Title"><segment><source>Open "file" C:\Temp</source><target state="translated">{{title}}</target></segment></unit>
</file>
</xliff>
""";
}
private static string NestedSourceXliff(string containingType)
{
return $$"""
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US">
<file id="TestApp.Localization.{{containingType}}+CommonLangResourceKind">
<unit id="Title"><segment><source>Title</source></segment></unit>
</file>
</xliff>
""";
}
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<TResourceKind>
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<TResourceKind> : LanguageCatalogDescriptor
where TResourceKind : struct, System.Enum
{
public LanguageCatalogDescriptor(
string catalogId,
System.Collections.Generic.IReadOnlyList<LanguageCatalogUnitDescriptor> units,
System.Func<TResourceKind, int> 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<string?> values) { }
}
}
namespace TestApp.Localization
{
[AtomUI.Localization.LanguageCatalog]
public enum LoginLangResourceKind
{
ItemCount,
Title
}
}
""";
private const string SourceXliff = """
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.1" srcLang="en-US">
<file id="TestApp.Localization.LoginLangResourceKind">
<unit id="ItemCount"><segment><source>Items {0}</source></segment></unit>
<unit id="Title"><segment><source>Open "file" C:\Temp</source></segment></unit>
</file>
</xliff>
""";
private const string ExpectedCatalogSource = """
// <auto-generated />
#nullable enable
namespace TestApp.Localization
{
public sealed class LoginLangResourceExtension
: global::AtomUI.Localization.LanguageResourceExtension<global::TestApp.Localization.LoginLangResourceKind>
{
public LoginLangResourceExtension()
{
}
public LoginLangResourceExtension(
global::TestApp.Localization.LoginLangResourceKind kind)
: base(kind)
{
}
}
}
""";
private const string ExpectedCatalogRegistrationSource = """
// <auto-generated />
#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<global::TestApp.Localization.LoginLangResourceKind>(
"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 = """
// <auto-generated />
#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);
}
}
}
""";
}