Flow.Launcher icon indicating copy to clipboard operation
Flow.Launcher copied to clipboard

DEMO: Localization source generators and analyzers

Open Yusyuriv opened this issue 9 months ago • 7 comments

[!WARNING] This is just a demonstration to gather feedback. This isn't necessarily going to make it into the codebase. This PR shouldn't be merged, at least in the current state.

The problem

Currently, localization is located in .xaml files and used in code like this:

InternationalizationManager.Instance.GetTranslation("checkUpdates");

Or like this, if the string has variables inside:

string.Format(
    InternationalizationManager.Instance.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
    locationFrom,
    locationTo
);

This has a few issues:

  1. It is error-prone. It's very easy to mistype a key in the string and not notice. Even if you don't mistype it, you could accidentally change it later and not notice.
  2. No autocomplete. There may be a string present in the XAML file already, and you roughly know its name, but you can't just start typing it for your IDE to suggest one of the existing strings. You will need to go to en.xaml, look it up there, and copy the key from there. It's very inconvenient.

Proposed solution

This PR implements a source generator that takes en.xaml file, parses it and generates a static class with methods. Each method's name is a key from en.xaml, and the return value is the translated string. Optionally, the method receives arguments if the translated value has placeholders ({0}). It also generates doc comments so you could hover a method and see what the actual string looks like. Here's an example of generated code:

namespace Current.Project.Namespace;

public static class Localize
{
/// <summary>
/// <code>
/// Check for Updates
/// </code>
/// </summary>
public static string checkUpdates() => InternationalizationManager.Instance.GetTranslation("checkUpdates");

/// <summary>
/// <code>
/// Become A Sponsor
/// </code>
/// </summary>
public static string BecomeASponsor() => InternationalizationManager.Instance.GetTranslation("BecomeASponsor");

/// <summary>
/// <code>
/// New version {version} is available, would you like to restart Flow Launcher to use the update?
/// </code>
/// </summary>
public static string newVersionTips(string version) => string.Format(InternationalizationManager.Instance.GetTranslation("newVersionTips"), version);
}

This is used like this:

var version = Localize.newVersionTips("1.0.0");

The Analyzers project includes several diagnostics and automatic fixes for them needed for the source generator to work. For example, when used in plugin projects, Localize class expects the main class of your plugin (the one implementing IPluginI18n) to have an internal or a public static property of type PluginInitContext. It will use that property in the generated code like this:

public static string BecomeASponsor() => Main.Context.GetTranslation("BecomeASponsor");

The analyzers will look at the main class of the plugin and show errors when it's impossible to generate the Localize class. A few examples:

This property is private and isn't static, so it's unavailable from the Localize class: image

This property is static, but it's still private, so it's unavailable from the Localize class: image

This is a private field, so it's unavailable from the Localize class: image

There's also an analyzer that warns about using the old localization method: image

All of these offer an automatic code fix (Alt+Enter in Rider).

To add the source generator and the analyzers to a project, this code should be added to .csproj file:

  <ItemGroup>
    <ProjectReference
      Include="..\..\Flow.Launcher.Analyzers\Flow.Launcher.Analyzers.csproj"
      ReferenceOutputAssembly="false"
      OutputItemType="Analyzer" />
    <ProjectReference
      Include="..\..\Flow.Launcher.SourceGenerators\Flow.Launcher.SourceGenerators.csproj"
      ReferenceOutputAssembly="false"
      OutputItemType="Analyzer" />
    <AdditionalFiles Include="Languages\en.xaml" />
  </ItemGroup>

<AdditionalFiles /> is needed because source generators don't have access to files unless they're specified in <AdditionalFiles />.

~~Unfortunately, I haven't managed to make analyzers work in Visual Studio, but they work well in Rider and VS Code.~~ Apparently, Visual Studio allows using only .NET Standard 2.0 for analyzers. Modified the code for .NET Standard 2.0, it now works in Visual Studio.

What's included in this PR

  1. The source generator
  2. The analyzers
  3. Flow.Launcher.Core project uses the source-generated Localize class everywhere it was using InternationalizationManager.Instance.GetTranslation()
  4. plugins/Flow.Launcher.Plugin.BrowserBookmark and plugins/Flow.Launcher.Plugin.Calculator have the source generator and the analyzers included, but no other changes introduced, so you could see for yourself how it works if you're interested to try. They're intentionally left in a broken state to show that the analyzer prevents them from building until the errors preventing the source generator from generating the Localize class are fixed.

What now

I don't know, I guess I just wanted to hear what you think about all this. It has been a great learning exercise for me either way.

Yusyuriv avatar May 25 '24 23:05 Yusyuriv