mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
488 lines
20 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
}
|