0

In Visual Studio you can create Shared Projects.

Have Microsoft envisioned any standard approach to localization in a shared project?

I'm pretty sure I can find a way to handle localization, but before I invent my own method, it would be nice to know if there is an "official" way.

EDIT

If you localize a library, the translations should be contained within the library.

If you localize texts using resource strings in a .resx (or .resw) file, then the resource file has to be included in the library, and there has to be code to load the resources from the rignt resource file.

Two things are not possible.

  1. Do the localization only in the component which uses the library.
    If you replace a string literal with a resource name, you are changing the source code, which is against the whole idea of using a library.

  2. Use resource names in a library, without providing a .resx file.
    That code just wouldn't work.

There is a similar question in the Microsoft forums. Based on the discussion on that page, it looks to me like Microsoft do not provide sufficient support for localization of shared projects.

I have found one interesting example, which is the project CodeMaid project on GitHub.

This is a Visual Studio Extension and not a Xamarin project (shared projects are not limited to Xamarin). This project contains a resource file (Resource.resx) and a code file (Resources.resx.cs) to access the resources. Based on this, I assume it is possible to include a .resx file in a shaered project.

However, it does not appear possible to run the custom tool (PublicResXFileCodeGenerator) to generate the code file, and it does not run automatically when you edit the .resx file.

My guess, is that the developers of CodeMaid have not made any changes to the resource file, since moving it into the shared project.

Phil Jollans
  • 3,605
  • 2
  • 37
  • 50
  • Is [Xamarin.Forms Localization](https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/localization/) what you mean? – Jessie Zhang -MSFT Jan 24 '22 at 02:31
  • I have added more detail to the question. What I mean is localization of the shared project, in the shared project itself, and not in the application. – Phil Jollans Jan 30 '22 at 12:03
  • Good idea,thanks for your explanation. If you can, please share your valuable ideas in more detail. Thanks in advance. :) – Jessie Zhang -MSFT Feb 08 '22 at 07:03

1 Answers1

0

The answer to my original question appears to be "No". With regard to localization, shared projects don't really work.

However, localization can work, but there are two problems.

1. The custom tool PublicResXFileCodeGenerator doesn't run.

In my own tool (which is a visual studio extension), I will try to generate the code behind (Resources.designer.cs) myself. The code required to generate it is quite simple.

This is my basic function ...

public void GenerateCodeCS ( string resxFullPath, string namespaceName, ITypeResolutionService typeResolver )
{
  string BaseName = Path.GetFileNameWithoutExtension ( resxFullPath ) ;
  string BaseDir  = Path.GetDirectoryName ( resxFullPath ) ;
  string FileName = BaseName + ".designer.cs" ;
  string FullPath = Path.Combine ( BaseDir, FileName ) ;

  using ( var sw = new StreamWriter ( FullPath, false, Encoding.UTF8 ) )
  {
    sw.WriteLine ( $"namespace {namespaceName}.Properties" ) ;
    sw.WriteLine ( "{" );
    sw.WriteLine ( "  using System;" );
    sw.WriteLine ( "  using System.Resources;" );
    sw.WriteLine ( "  using System.Globalization;" );
    sw.WriteLine ( "  public class Resources" );
    sw.WriteLine ( "  {" );
    sw.WriteLine ( "    private static ResourceManager resourceMan;" );
    sw.WriteLine ( "    private static CultureInfo     resourceCulture;" );
    sw.WriteLine ();
    sw.WriteLine ( "    public static global::System.Resources.ResourceManager ResourceManager" );
    sw.WriteLine ( "    {" );
    sw.WriteLine ( "      get" );
    sw.WriteLine ( "      {" );
    sw.WriteLine ( "        if ( resourceMan == null )" );
    sw.WriteLine ( "        {" );
    sw.WriteLine ($"          resourceMan = new ResourceManager(\"{namespaceName}.Properties.Resources\", typeof(Resources).Assembly);" );
    sw.WriteLine ( "        }" );
    sw.WriteLine ( "        return resourceMan ;" );
    sw.WriteLine ( "      }" );
    sw.WriteLine ( "    }" );
    sw.WriteLine ();
    sw.WriteLine ( "    public static CultureInfo Culture { get => resourceCulture; set => resourceCulture = value; }" );
    sw.WriteLine ();

    using ( var resxReader = new ResXResourceReader ( resxFullPath, typeResolver ) )
    {
      resxReader.BasePath         = BaseDir ;
      resxReader.UseResXDataNodes = true ;

      foreach ( DictionaryEntry res in resxReader as IResourceReader )
      {
        var key      = res.Key.ToString() ;
        var resxNode = res.Value as ResXDataNode ;
        var value    = resxNode.GetValue ( typeResolver ) ;
        if ( value is string )
        {
          sw.WriteLine ( $"    public static string {key} => ResourceManager.GetString(\"{key}\", resourceCulture);" ) ;
        }
      }
    }
    sw.WriteLine ( "  }" );
    sw.WriteLine ( "}" );
  }
}

This generates a file something like this

namespace SharedProject.Properties
{
  using System;
  using System.Resources;
  using System.Globalization;
  public class Resources
  {
    private static ResourceManager resourceMan;
    private static CultureInfo     resourceCulture;

    public static global::System.Resources.ResourceManager ResourceManager
    {
      get
      {
        if ( resourceMan == null )
        {
          resourceMan = new ResourceManager("SharedProject.Properties.Resources", typeof(Resources).Assembly);
        }
        return resourceMan ;
      }
    }

    public static CultureInfo Culture { get => resourceCulture; set => resourceCulture = value; }

    public static string Bye_Bye => ResourceManager.GetString("Bye_Bye", resourceCulture);
    public static string Ciao_Ciao => ResourceManager.GetString("Ciao_Ciao", resourceCulture);
    public static string Hello_World => ResourceManager.GetString("Hello_World", resourceCulture);
  }
}

It should be possible to include that in a console app and to run it in a pre-build step. The typeResolver parameter is probably not necessary.

2. The namespace for the resources is wrong.

When the resources are compiled into the target application, they are given the namespace of the application, and not the namespace of the shared project.

Since the code in the shared project cannot possibly know the namespace of the target application, this makes it impossible to access the resources.

Fortunately, there is a workaround for this problem. What you have to do, is to edit the project file and add the <LogicalName> property to the resource file.

Actually, in a shared project, it is not the project file (.csproj), but the file project items file (.projitems).

In my example project, the resource file is referenced like this

<EmbeddedResource Include="$(MSBuildThisFileDirectory)Properties\Resources.resx">
  <Generator>PublicResXFileCodeGenerator</Generator>
  <LogicalName>SharedProject.Properties.Resources.resources</LogicalName>
</EmbeddedResource>

In this case, SharedProject is the namespace of the shared project, which is referenced by the code behind (see above).

Of course the <Generator> property is pointless, because the custom tool does not run.

I am not aware of any way to add the property via the GUI or even from a visual studio extension.

Using ILSpy, I can see that the resources are now embedded into my application with the namespace of the shared project

enter image description here

and loading the resources works.

Phil Jollans
  • 3,605
  • 2
  • 37
  • 50