Prologue
To begin with, over many years of work as a programmer, I have repeatedly come across the task of introducing into a localization project in one form or another, but usually these were solutions that work on the basis of a loadable dictionary with key-value pairs. This approach is fully justified for small projects, but it has several significant drawbacks:
- The complexity of the implementation of an existing project.
- Lack of formatting tools for localized messages (except for standard string.Format).
- The impossibility of embedding culturally-dependent functions. For example, a typical task — the substitution of the desired form of a word depending on the meaning of a number — cannot be resolved by dictionaries alone.
After analyzing these problems, I came to the conclusion that it was necessary to create my own library for localizing projects, which would be deprived of the above disadvantages. In this article I will talk about the principles of its work with examples of code in C #.
')
Library composition
Link to SourceForge project:
https://sourceforge.net/projects/open-genesis/?source=navbarExample:
LocalizationViewerThe assembly includes the following projects:
- Genesis.Localization is the main localization library.
- Ru - the implementation of Russian localization (example).
- En - the implementation of the English localization (example).
- LocalizationViewer - a program to demonstrate the capabilities of the library with the ability to edit localizations.

Basic principles

Localization Manager
The library is built on the basis of plug-ins and works as follows: when the application is started, a localization manager (
LocalizationManager ) is created, which specifies the path to the directory where it will search for available localization packages (
LocalizationPackage ), each of which is responsible for a certain culture (Russian localization package, English, etc.). After that, a command is given to search and download descriptors of all packages, all initialization code looks like this:
If everything went smoothly, a list of available localizations will appear in the manager in the form of their brief descriptions (descriptors,
LocalizationDescriptor ). These descriptors do not contain any logic, but serve only as a description of a package that can be downloaded and started to be applied in the program.
A list of all localizations can be obtained from the manager:
manager.Localizations
For example, we wanted to connect the Russian localization, for this you need to load it directly into the manager:
LocalizationPackage package = manager.Load("ru");
After loading, you can work with localization — get strings, resources, etc. from it, and if it is no longer needed, you can unload it:
manager.Unload("ru");
Important! You can download and upload an unlimited number of localizations, because they are all created in their own domains (AppDomain).
Localization package
Each localization is a set of files in a separate directory, the root for all is the one that was selected when the localization manager was loaded. In the example above, this will be the
[ProjectDir] \ Localization directory, and the localization packages will be located in the
[ProjectDir] \ Localization \ ru ,
[ProjectDir] \ Localization \ en directories , etc ...
Each standard package must contain the following files:

- localization.info is an xml file with a brief description of the package; these are the files that the localization manager initially loads.
Example for Russian localization:
<?xml version="1.0"?> <LocalizationInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Name></Name> <Culture>ru</Culture> </LocalizationInfo>
As you can see, there are only two fields here, maybe later new fields will be added to identify one or another package. - flag.png - image symbolizing localization. In my examples, these are 16x16 pixel state flags.
- strings.xml - an xml file containing localized strings. When redefining the package logic, you can create your own source of strings, for example, a binary or database.
- package.dll - the executable module of the package is a small library in which there must be a class inherited from LocalizationPackage .
An example of executable code for Russian localization:
using System; using Genesis.Localization; namespace Ru { public class Package : LocalizationPackage { protected override object Plural(int count, params object[] forms) { int m10 = count % 10; int m100 = count % 100; if (m10 == 1 && m100 != 11) { return forms[0]; } else if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) { return forms[1]; } else { return forms[2]; } } } }
The following will explain what the Plural method is.
Application of localization packages
So, we created a localization manager and uploaded the package with the translation into it. Now it can be used in the program in three ways:
- Getting a specific string by its key (classic method). The key can be a string or an Int32 type number.
Usage example:
LocalizationPackage package = manager.Load(culture); string strByName = package["name"]; string strByID = package[150];
- Receiving a formatted string with passing arguments. This is the method for which the library was created.
Usage example:
LocalizationPackage package = manager.Load(culture); string formattedString = package["name", arg1, args2, ...];
As arguments any objects can be used. More on this method will be described below.
- Getting the path to a localized resource. To do this, use the GetResourceFilePath (string filename) method to get the path to an arbitrary file in the localization directory or the GetImage (string filename) method to load an image from there.
String interpreter
The power of the library lies in its string interpreter. What is he like?
In short, this is a set of instructions included in localized strings, with which you can adapt a translation for a particular culture.
The string interpreter is called by the method described above to get a string with the specified arguments (during normal key access, the localized string is returned in “pure” form) or by the special GetFormattedString (string format, params object [] args) method, which works in the same way, but An arbitrary format string is passed as the first argument.
Now more about these instructions. There are two of them:
- Include argument in string.
Instruction format: %index%
Result: embedding the argument under the index number
Usage example:
package.GetFormattedString("%1% = %0%%%", 80, "");
Result:
= 80%
Please note that the % symbol, being a service one, must be escaped by another same symbol as in this example.
- Enable features
Instruction format:
%Func(arg1, arg2, ..., argN)%
Arguments can be numbers , strings in double quotes ( quotes themselves are escaped, like%, double repetition), string arguments by their number (% index), or calls to other functions .
Usage example:
package.GetFormattedString(" %1% %Upper(Random(\"\", \"\", \"\"))% , %0% %Plural(%0, \"\", \"\", \"\")% .", 55, "MegaDeath2000");
Result:
MegaDeath2000 , 55 .
Built-in functions and integration
The
LocalizationPackage class has several “standard” functions built in, a part was used in the example above:
- Plural (int, var1, var2, ..., varN) - embedding the form of the word depending on the number, this method is unique for each culture and must be redefined. In particular, in Russian there are three forms of number (for example: "1 unit", "2 units", "8 units").
- Random (var1, var2, ..., varN) - selection of a random value among the specified ones.
- Upper (string) - coercion to upper case.
- Lower (string) - reduction to lower case.
- UpperF (string) - coercion to uppercase only the first letter ("word" => "Word").
- LowerF (string) - lowercase the first letter only.
If you need to add new features, you can do this in two ways.
- In the redefined package class, you can declare new functions and mark them with the [Function] attribute, then they will be automatically included in the interpreter for a specific localization. The built-in functions are defined in this way, for example, the functions Plural and Random look like:
[Function("P")] protected abstract object Plural(int count, params object[] forms); [Function] protected virtual object Random(params object[] variants) { if (variants.Length == 0) { return null; } else { return variants[_rnd.Next(variants.Length)]; } }
Please note that it is permissible for a function to specify a list of its aliases (for short entry), for example, Plural can be called either via the main name ( Plural ) or through an alias ( P ), and the register in the function names does not matter.
- Integration of eigenfunctions, for this purpose, the InjectFormatterFunction method is used , example of use:
var package = LocalizationManager.Load("ru"); package.InjectFormatterFunction(new Func<int, int, int>((a, b) => Math.Min(a, b)), "Min"); package.InjectFormatterFunction(new Func<int, int, int>((a, b) => Math.Max(a, b)), "Max"); package.GetFormattedString("%min(%0, max(%1, %2))%", 10, 8, 5);
Result:
8
As an argument for the InjectFormatterFunction, a method (MethodInfo) or a delegate can be passed (in the example above, delegates are passed).
Additional features
In addition to the basic functions, the library provides two more additions.
Debug mode
The Debug-version of the library includes the ability not only to get localized strings using the methods described above, but also to write them directly:
var package = LocalizationManager.Load("ru"); package["New Key"] = " "; package.Save();
In this case, a new localized string will be created with the specified key and value (or the existing one will be overwritten), and the package itself will be saved to disk. Also in debug mode, if you try to read a line with a missing key, an empty value will be returned, but a new record will be created. This is convenient at the initial stage of development - we do not need to worry about filling the dictionary - it will be filled with empty values, which we then fill with data.
In the release, recording functions are not available, which is quite logical - an industrial program should not be able to replenish its localization dictionary.
Mappings
This is our dessert. Purpose - rapid localization of forms, controls and other complex objects.
This feature is used in the demo project
LocalizationViewer .
I will give an excerpt of the description of the main form:
[LocalizableClass("Text", "CAPTION")] public partial class frmMain : Form { ... [LocalizableClass] private System.Windows.Forms.ToolStripButton cmdExit; [LocalizableClass] private System.Windows.Forms.ToolStripButton cmdSave; [LocalizableClass] private System.Windows.Forms.ToolStripLabel lblSearch; ...
LocalizationMapper , allows you to localize any object passed to it in the
Localize function, using the [Localizable] and [LocalizableClass] attributes in the fields and properties of the object being localized (in this case, the form). For example, the attribute [LocalizableClass] without parameters means that the default property (Text) should be localized, and an automatic key of the form <class>. <Subclass>. <Field> will be used. For the Text field of the cmdExit button, the key will be as follows:
LocalizationViewer.frmMain.cmdExit_Text
Conclusion
The library will soon be tested in one of my projects, so there will most likely be some improvements, mainly aimed at expanding the basic functionality of the packages. Stay tuned for updates on SourceForge and write your comments and thoughts on the further development of the library.
PS
You might say that I reinvent the wheel. Let it be so, but reinventing bicycles is my hobby ...
In addition, it is much more interesting and useful in terms of self-improvement in programming.
For the same reason, there will be no references to literature and other sources of information - everything was written from scratch.
My other publications
- Filling text templates with model-based data. Implementing on .NET using dynamic functions in bytecode (IL)