Skip to content

Commit

Permalink
Merge pull request #61 from smoogipoo/file-namespace
Browse files Browse the repository at this point in the history
Add support for adjusting the localisation namespace
  • Loading branch information
peppy authored Mar 18, 2024
2 parents 2f4a0d3 + fbf3012 commit 117c350
Show file tree
Hide file tree
Showing 22 changed files with 258 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public class LocaliseClassStringCodeFixProviderTests : AbstractCodeFixProviderTe
[InlineData("VerbatimString")]
[InlineData("InterpolatedString")]
[InlineData("InterpolatedStringWithQuotes")]
[InlineData("CustomPrefix")]
[InlineData("CustomPrefixNamespace")]
[InlineData("CustomResourceNamespace")]
[InlineData("CustomLocalisationNamespace")]
[InlineData("NestedClass")]
[InlineData("LongString")]
[InlineData("LicenseHeader")]
Expand All @@ -27,7 +29,8 @@ public class LocaliseClassStringCodeFixProviderTests : AbstractCodeFixProviderTe
public async Task Check(string name) => await RunTest(name);

[Theory]
[InlineData("CustomPrefix")]
[InlineData("CustomPrefixNamespace")]
[InlineData("CustomFileNamespace")]
public async Task CheckWithBrokenAnalyzerConfigFiles(string name) => await RunTest(name, true);

protected override Task Verify((string filename, string content)[] sources, (string filename, string content)[] fixedSources, bool brokenAnalyserConfigFiles = false)
Expand Down
16 changes: 8 additions & 8 deletions LocalisationAnalyser.Tests/Localisation/LocalisationFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public async Task ClassGeneratedForNoFile()
[Fact]
public async Task EmptyFileContainsNoMembers()
{
var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down Expand Up @@ -78,7 +78,7 @@ public async Task CheckPropertyMemberIsReadCorrectly()
const string key_name = "TestKey";
const string english_text = "TestEnglish";

var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down Expand Up @@ -134,7 +134,7 @@ public async Task CheckMethodMemberIsReadCorrectly()
var param2 = new LocalisationParameter("string", "second");
var param3 = new LocalisationParameter("customobj", "third");

var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down Expand Up @@ -168,7 +168,7 @@ public async Task CheckVerbatimStringIsConvertedToLiteral()
const string prop_name = "TestProperty";
const string key_name = "TestKey";

var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down Expand Up @@ -292,7 +292,7 @@ public async Task XmlDocIsHtmlDecoded()
const string prop_name = "TestProperty";
const string key_name = "TestKey";

var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand All @@ -318,7 +318,7 @@ public async Task XmlDocWithNewlines()
const string prop_name = "TestProperty";
const string key_name = "TestKey";

var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down Expand Up @@ -352,7 +352,7 @@ public async Task XmlDocWithXmlEntities()
const string prop_name = "TestProperty";
const string key_name = "TestKey";

var localisation = await setupFile($@"{SyntaxTemplates.FILE_HEADER_TEMPLATE}
var localisation = await setupFile($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down Expand Up @@ -393,7 +393,7 @@ private void checkResult(string inner)
{
var sb = new StringBuilder();

sb.Append($@"{string.Format(SyntaxTemplates.FILE_HEADER_TEMPLATE, string.Empty)}
sb.Append($@"using {SyntaxTemplates.FRAMEWORK_LOCALISATION_NAMESPACE};
namespace {test_namespace}
{{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using osu.Framework.Localisation;

namespace TestProject.Localisation.Sub
{
public static class ProgramStrings
{
private const string prefix = @"TestProject.Localisation.Program";

/// <summary>
/// "abc"
/// </summary>
public static LocalisableString Abc => new TranslatableString(getKey(@"abc"), @"abc");

private static string getKey(string key) => $@"{prefix}:{key}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using TestProject.Localisation.Sub;

namespace Test
{
class Program
{
static void Main()
{
string x = ProgramStrings.Abc;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*.cs]
dotnet_diagnostic.OLOC001.localisation_namespace = Localisation.Sub
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Test
{
class Program
{
static void Main()
{
string x = [|"abc"|];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using osu.Framework.Localisation;

namespace TestProject.Localisation
{
public static class ProgramStrings
{
private const string prefix = @"CustomNamespace.Program";

/// <summary>
/// "abc"
/// </summary>
public static LocalisableString Abc => new TranslatableString(getKey(@"abc"), @"abc");

private static string getKey(string key) => $@"{prefix}:{key}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using TestProject.Localisation;

namespace Test
{
class Program
{
static void Main()
{
string x = ProgramStrings.Abc;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*.cs]
dotnet_diagnostic.OLOC001.resource_namespace = CustomNamespace
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Test
{
class Program
{
static void Main()
{
string x = [|"abc"|];
}
}
}
2 changes: 1 addition & 1 deletion LocalisationAnalyser.Tools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private static async Task projectToResX(FileInfo projectFile, DirectoryInfo? out
var workspace = MSBuildWorkspace.Create();
var project = await workspace.OpenProjectAsync(projectFile.FullName);

var localisationFiles = project.Documents.Where(d => d.Folders.FirstOrDefault() == SyntaxTemplates.PROJECT_RELATIVE_LOCALISATION_PATH)
var localisationFiles = project.Documents.Where(d => d.Folders.FirstOrDefault() == SyntaxTemplates.DEFAULT_LOCALISATION_NAMESPACE)
.Where(d => d.Name.EndsWith(".cs"))
.Where(d => Path.GetFileNameWithoutExtension(d.Name).EndsWith(SyntaxTemplates.STRINGS_FILE_SUFFIX))
.ToArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using LocalisationAnalyser.Abstractions.IO;
using LocalisationAnalyser.Extensions;
using LocalisationAnalyser.Localisation;
using LocalisationAnalyser.Utils;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -274,17 +275,17 @@ private async Task<Solution> addMember(Document document, SyntaxNode nodeToRepla
if (containingType == null)
throw new InvalidOperationException("String is not within a class, struct, or enum type.");

var projectDirectory = fileSystem.Path.GetDirectoryName(project.FilePath!);
var localisationDirectory = fileSystem.Path.Combine(new[] { projectDirectory }.Concat(SyntaxTemplates.PROJECT_RELATIVE_LOCALISATION_PATH.Split('/')).ToArray());
string localisationNamespace = options.GetLocalisationNamespace(SyntaxTemplates.DEFAULT_LOCALISATION_NAMESPACE);

// The class being localised.
var incomingClassName = containingType.Identifier.Text;
string? incomingClassName = containingType.Identifier.Text;

// The localisation class.
var localisationNamespace = $"{project.AssemblyName}.{SyntaxTemplates.PROJECT_RELATIVE_LOCALISATION_PATH.Replace('/', '.')}";
var localisationName = GetLocalisationFileName(containingType.Identifier.Text);
var localisationFileName = fileSystem.Path.Combine(localisationDirectory, fileSystem.Path.ChangeExtension(localisationName, "cs"));
var localisationFile = fileSystem.FileInfo.FromFileName(localisationFileName);
string projectDirectory = fileSystem.Path.GetDirectoryName(project.FilePath!);
string localisationDirectory = fileSystem.Path.Combine(new[] { projectDirectory }.Concat(localisationNamespace.Split('.')).ToArray());
string localisationFileName = BuildLocalisationName(containingType.Identifier.Text);
string localisationFilePath = fileSystem.Path.Combine(localisationDirectory, fileSystem.Path.ChangeExtension(localisationFileName, "cs"));
IFileInfo localisationFile = fileSystem.FileInfo.FromFileName(localisationFilePath);

if (localisationFile.Exists)
{
Expand All @@ -298,34 +299,32 @@ private async Task<Solution> addMember(Document document, SyntaxNode nodeToRepla
}
}

// The prefix namespace defaults to the localisation class' namespace, but can be customised via .editorconfig.
var prefixNamespace = localisationNamespace;

if (options != null && options.TryGetValue($"dotnet_diagnostic.{DiagnosticRules.STRING_CAN_BE_LOCALISED.Id}.prefix_namespace", out var customPrefixNamespace))
{
if (string.IsNullOrEmpty(customPrefixNamespace))
throw new InvalidOperationException("Custom namespace cannot be empty.");

prefixNamespace = customPrefixNamespace;
}

return (localisationFile, new LocalisationFile(localisationNamespace, localisationName, GetLocalisationPrefix(prefixNamespace, incomingClassName)));
return
(
localisationFile,
new LocalisationFile(
$"{project.AssemblyName}.{localisationNamespace}",
localisationFileName,
BuildResourcePrefix(
options.GetResourceNamespace($"{project.AssemblyName}.{SyntaxTemplates.DEFAULT_LOCALISATION_NAMESPACE}"),
incomingClassName))
);
}

/// <summary>
/// Retrieves "prefix" value for the localisation class corresponding to a given class name and namespace.
/// Builds the "prefix" value for the localisation class.
/// </summary>
/// <param name="namespace">The namespace in which the localisation class will be placed.</param>
/// <param name="className">The name of the original class.</param>
/// <returns>The prefix value.</returns>
protected virtual string GetLocalisationPrefix(string @namespace, string className) => $"{@namespace}.{className}";
/// <param name="namespace">The resource namespace.</param>
/// <param name="className">The name of the class being localised.</param>
/// <returns>The final prefix value.</returns>
protected virtual string BuildResourcePrefix(string @namespace, string className) => $"{@namespace}.{className}";

/// <summary>
/// Retrieves the name of the localisation class corresponding to a given class name.
/// Builds the name for a localisation class.
/// </summary>
/// <param name="className">The name of the original class.</param>
/// <param name="className">The name of the class being localised.</param>
/// <returns>The name of the localisation class corresponding to <paramref name="className"/>.</returns>
protected virtual string GetLocalisationFileName(string className) => className;
protected virtual string BuildLocalisationName(string className) => className;

/// <summary>
/// Whether to add the resultant localisation class file to the workspace.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public LocaliseClassStringCodeFixProvider(IFileSystem fileSystem)
{
}

protected override string GetLocalisationFileName(string className) => $"{base.GetLocalisationFileName(className)}{SyntaxTemplates.STRINGS_FILE_SUFFIX}";
protected override string BuildLocalisationName(string className) => $"{base.BuildLocalisationName(className)}{SyntaxTemplates.STRINGS_FILE_SUFFIX}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public LocaliseCommonStringCodeFixProvider(IFileSystem fileSystem)
{
}

protected override string GetLocalisationPrefix(string @namespace, string className) => base.GetLocalisationPrefix(@namespace, SyntaxTemplates.COMMON_STRINGS_CLASS_NAME);
protected override string BuildResourcePrefix(string @namespace, string className) => base.BuildResourcePrefix(@namespace, SyntaxTemplates.COMMON_STRINGS_CLASS_NAME);

protected override string GetLocalisationFileName(string className) => $"{SyntaxTemplates.COMMON_STRINGS_CLASS_NAME}{SyntaxTemplates.STRINGS_FILE_SUFFIX}";
protected override string BuildLocalisationName(string className) => $"{SyntaxTemplates.COMMON_STRINGS_CLASS_NAME}{SyntaxTemplates.STRINGS_FILE_SUFFIX}";
}
}
82 changes: 82 additions & 0 deletions LocalisationAnalyser/Extensions/AnalyzerConfigOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using Microsoft.CodeAnalysis.Diagnostics;

namespace LocalisationAnalyser.Extensions
{
public static class AnalyzerConfigOptionsExtensions
{
/// <summary>
/// Retrieves the namespace under which the localisation strings are expected to be placed.
/// </summary>
/// <param name="config">The config that may provide a custom value via <c>localisation_namespace</c>.</param>
/// <param name="fallback">The fallback value in case no custom value is provided.</param>
/// <returns>The localisation strings namespace - either a custom value from <paramref name="config"/>, or the given <paramref name="fallback"/>.</returns>
/// <exception cref="InvalidOperationException">If the config contained an empty string as a custom value.</exception>
public static string GetLocalisationNamespace(this AnalyzerConfigOptions? config, string fallback)
{
if (config == null)
return fallback;

if (config.TryGetValue($"dotnet_diagnostic.{DiagnosticRules.STRING_CAN_BE_LOCALISED.Id}.localisation_namespace", out string? customFileNamespace))
{
if (string.IsNullOrEmpty(customFileNamespace))
throw new InvalidOperationException("Custom localisation namespace cannot be empty.");

return customFileNamespace;
}

return fallback;
}

/// <summary>
/// Retrieves the namespace under which the localisation resources are expected to be found.
/// </summary>
/// <param name="config">The config that may provide a custom value via either <c>resource_namespace</c> or <c>prefix_namespace</c>.</param>
/// <param name="fallback">The fallback value in case no custom value is provided.</param>
/// <returns>The localisation resource namespace - either a custom value from <paramref name="config"/>, or the given <paramref name="fallback"/>.</returns>
/// <exception cref="InvalidOperationException">If the config contained an empty string as a custom value.</exception>
public static string GetResourceNamespace(this AnalyzerConfigOptions? config, string fallback)
{
if (config == null)
return fallback;

if (config.TryGetValue($"dotnet_diagnostic.{DiagnosticRules.STRING_CAN_BE_LOCALISED.Id}.resource_namespace", out string? customResourceNamespace))
{
if (string.IsNullOrEmpty(customResourceNamespace))
throw new InvalidOperationException("Custom resource namespace cannot be empty.");

return customResourceNamespace;
}

if (config.TryGetValue($"dotnet_diagnostic.{DiagnosticRules.STRING_CAN_BE_LOCALISED.Id}.prefix_namespace", out string? customPrefixNamespace))
{
if (string.IsNullOrEmpty(customPrefixNamespace))
throw new InvalidOperationException("Custom prefix namespace cannot be empty.");

return customPrefixNamespace;
}

return fallback;
}

public static string[]? GetLicenseHeader(this AnalyzerConfigOptions? config)
{
if (config == null)
return null;

if (!config.TryGetValue($"dotnet_diagnostic.{DiagnosticRules.STRING_CAN_BE_LOCALISED.Id}.license_header", out string licenseHeader))
return null;

if (string.IsNullOrEmpty(licenseHeader))
return null;

if (!licenseHeader.StartsWith("//", StringComparison.Ordinal))
licenseHeader = $"// {licenseHeader}";

return licenseHeader.Split(new[] { "\\n" }, StringSplitOptions.None);
}
}
}
Loading

0 comments on commit 117c350

Please sign in to comment.