Reputation: 496
ResourceManager
and ResXFileCodeGenerator
provide great functionality for localization: it is enough to create the same .resx file with a language prefix (e.g. MyStrings.ru.resx) and as a result we can smoothly work with different languages by setting corresponding MyStrings.Culture property in generated Designer file and invoking necessary string-related property:
MyStrings.Culture = new CultureInfo("ru");
Console.Write(MyStrings.MyTranslatedString); // Russian output
MyStrings.Culture = CultureInfo.InvariantCulture;
Console.Write(MyStrings.MyTranslatedString); // English output
I like this approach very well. But unfortunately it will fail in multithreading mode, because mentioned .Culture property is static.
I want to keep the same functionality (easy resource files edit; automatically generated properties with Inlellisense support, etc.), but with ability to work in multithreading mode.
Of course I can use ResourceManager
directly, like that:
ResourceManager.GetString("Commands description", resourceCulture);
But in this case if I change a name (key) in a .resx file, I will have to change it manually in .cs files, which is not convenient enough.
Upvotes: 3
Views: 330
Reputation: 496
Raw solution for converting auto-generated static designer class to instance-based is to create T4 template (ThreadSafeTranslation.tt file):
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
var fileName = "TranslationBase.Designer.cs";
var oldName = "TranslationBase";
var newName = "Translation";
var resourcePath = this.Host.ResolvePath(fileName);
var code = File.ReadAllText(resourcePath);
code = code.Replace($"internal class {oldName} {{", $"internal class {newName} {{");
code = code.Replace($"internal {oldName}() {{", $"internal {newName}() {{");
code = code.Replace($"({oldName}).Assembly);", $"({newName}).Assembly);");
code = code.Replace("private static global::System.Globalization.CultureInfo resourceCulture;",
"private global::System.Globalization.CultureInfo resourceCulture;");
code = code.Replace("internal static global::System.Globalization.CultureInfo Culture {",
"internal global::System.Globalization.CultureInfo Culture {");
code = code.Replace("internal static string ", "internal string ");
#>
<#=code#>
Also I edited .cproj to make this T4 template executable on each build (I don't know how to trigger it on changing dependent file only):
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command=""$(DevEnvDir)TextTransform.exe" -out "$(ProjectDir)Translations\ThreadSafeTranslation.cs" "$(ProjectDir)Translations\ThreadSafeTranslation.tt"" />
</Target>
But this approach has obvious defect: when I changing a key in .resx, it is automatically reflected in a class, generated by ResXFileCodeGenerator (and in all references to that property if so -- that is awesome!), but it is not changed in references to my generated class... :( I have to look through the code and manually fix all references to previous field.
But of course, seeing these errors on early stage is much better than getting runtime errors if you have to operate directly with string values.
Upvotes: 0
Reputation: 311185
In your example, the property MyStrings.MyTranslatedString
is defined in generated code. To achieve what you are asking for, you'd need to generate your own implementation of that file.
You could achieve this using a source generator, although there are quite a few steps involved in doing that.
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview
Alternatively you could hand-code a class to do this. Perhaps you could add a unit test to ensure that all keys in your .resx
file are also present in your hand-coded class, to catch cases where it gets out of sync. That'd be less work than a source generator, but require more ongoing maintenance. If your resources don't change that frequently, it might be the better option.
Upvotes: 2