Локализация проектов на .NET с интерпретатором функций

от автора

Пролог

Начну с того, что за много лет работы программистом я неоднократно сталкивался с задачей внедрения в проект локализации в том или ином виде, но обычно это были решения, работающие на основе подгружаемого словаря с парами ключ-значение. Такой подход вполне оправдан для небольших проектов, но имеет ряд существенных недостатков:

  1. Сложность внедрения в существующий проект.
  2. Отсутствие средств форматирования локализованных сообщений (за исключением стандартного string.Format).
  3. Невозможность встраивания культурно-зависимых функций. Например, типичную задачу, — подстановку нужной формы слова в зависимости от значения числа, — одними словарями значений не разрешить.

Проанализировав эти проблемы, я пришел к выводу о необходимости создания собственной библиотеки для локализации проектов, которая будет лишена перечисленных выше недостатков. В этой статье я расскажу о принципах ее работы с примерами кода на C#.

Состав библиотеки

Ссылка на проект SourceForge: https://sourceforge.net/projects/open-genesis/?source=navbar

В сборку входят следующие проекты:

  • Genesis.Localization — основная библиотека локализации.
  • Ru — реализация русской локализации (пример).
  • En — реализация английской локализации (пример).
  • LocalizationViewer — программа для демонстрации возможностей библиотеки с возможностью редактирования локализаций.

    image

Основные принципы

image

Менеджер локализаций

Библиотека построена на базе плагинов и работает следующим образом: при запуске приложения создается менеджер локализаций (LocalizationManager), которому указывается путь к каталогу, где он будет производить поиск доступных пакетов локализаций (LocalizationPackage), каждый из которых отвечает за некоторую культуру (пакет русской локализации, английской и т.п.). После этого дается команда на поиск и загрузку дескрипторов всех пакетов, весь код инициализации выглядит примерно следующим образом:

// инициализация менеджера локализаций LocalizationManager = new LocalizationManager(); LocalizationManager.BasePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Localization"); LocalizationManager.Initialize(); try {     LocalizationManager.DetectAllLocalizations(); } catch (LocalizationException ex) {     MessageBox.Show(ex.Message, "Ошибка локализации", MessageBoxButtons.OK, MessageBoxIcon.Error);     return; } 

Если все прошло без ошибок, в менеджере появится список доступных локализаций в виде их кратких описаний (дескрипторов, LocalizationDescriptor). Эти дескрипторы не содержат в себе какой-либо логики, а служат только лишь описанием того или иного пакета, который можно загрузить и начать применять в программе.

Список всех локализаций можно получить из менеджера:

manager.Localizations 

Например, мы захотели подключить русскую локализацию, для этого необходимо загрузить ее непосредственно в менеджер:

LocalizationPackage package = manager.Load("ru"); 

После загрузки с локализацией можно работать — получать из нее строки, ресурсы и т.д., а если она более не нужна, ее можно выгрузить:

manager.Unload("ru"); 

Важно! Можно загружать и выгружать неограниченное число локализаций, т.к. все они создаются в собственных доменах (AppDomain).

Пакет локализации

Каждая локализация представляет из себя набор файлов в отдельном каталоге, корневым для всех является тот, который был выбран при загрузке менеджера локализаций. В примере выше, это будет каталог [ProjectDir]\Localization, а непосредственно пакеты локализаций будут размещаться в каталогах [ProjectDir]\Localization\ru, [ProjectDir]\Localization\en и т.д…

Каждый стандартный пакет, обязательно должен содержать следующие файлы:

image

  • localization.info — xml файл с кратким описанием пакета, именно эти файлы изначально загружает менеджер локализаций.

    Пример для русской локализации:

    <?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> 

    Как видим, здесь всего два поля, возможно потом будут добавлены новые поля для идентификации того или иного пакета.

  • flag.png — изображение, символизирующее локализацию. В моих примерах это флаги государств размером 16×16 пикселей.
  • strings.xml — xml файл, содержащий в себе локализованные строки. При переопределении логики пакета можно создать свой собственный источник строк, например, бинарник или базу данных.
  • package.dll — исполняемый модуль пакета — небольшая библиотечка, в которой должен присутствовать класс, унаследованный от LocalizationPackage.

    Пример исполняемого кода для русской локализации:

    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];             }         }     } } 

    Ниже будет дано пояснение, что такое метод Plural.

Применение пакетов локализации

Итак, мы создали менеджер локализаций и загрузили в него пакет с переводом. Теперь его можно использовать в программе тремя способами:

  1. Получение конкретной строки по ее ключу (классический метод). В роли ключа может выступать строка или число типа Int32.

    Пример использования:

    LocalizationPackage package = manager.Load(culture); string strByName = package["name"]; string strByID = package[150]; 

  2. Получение форматированной строки с передачей аргументов. Это тот метод, ради которого и создавалась библиотека.

    Пример использования:

    LocalizationPackage package = manager.Load(culture); string formattedString = package["name", arg1, args2, ...]; 

    В качестве аргументов могут использоваться любые объекты. Подробнее об этом методе будет рассказано ниже.

  3. Получение пути к локализованному ресурсу. Для этого используется метод GetResourceFilePath(string filename) для получения пути к произвольному файлу в каталоге локализации или метод GetImage(string filename) для загрузки изображения оттуда же.

Интерпретатор строк

Сила библиотеки заключается в ее интерпретаторе строк. Что же он из себя представляет?
Если вкратце, то это набор инструкций, включаемых в локализованные строки, с помощью которых можно адаптировать перевод под ту или иную культуру.

Интерпретатор строк вызывается описанным выше методом получения строки с заданными аргументами (при обычном обращении по ключу возвращается локализованная строка в «чистом» виде) или специальным методом GetFormattedString(string format, params object[] args), который работает точно так же, но при этом в качестве первого аргумента передается произвольная строка формата.

Теперь подробнее об этих инструкциях. Всего их две:

  1. Включение аргумента в строку.

    Формат инструкции:

    %index% 

    Результат: встраивание в строку аргумента под номером index

    Пример использования:

    package.GetFormattedString("%1% = %0%%%", 80, "КПД"); 

    Результат:

    КПД = 80% 

    Обратите внимание, что символ %, будучи служебным, должен экранироваться другим таким же символом, как в этом примере.

  2. Включение функций

    Формат инструкции:

    %Func(arg1, arg2, ..., argN)% 

    Аргументами могут быть числа, строки в двойных кавычках (сами кавычки экранируются, как и %, двойным повтором), аргументы строки по их номеру (%index) или вызовы других функций.

    Пример использования:

    package.GetFormattedString("Игрок %1% наносит по вам %Upper(Random(\"сильный\", \"сокрушительный\", \"мощный\"))% удар, отнимая %0% %Plural(%0, \"единицу\", \"единицы\", \"единиц\")% здоровья.", 55, "MegaDeath2000"); 

    Результат:

    Игрок MegaDeath2000 наносит по вам СОКРУШИТЕЛЬНЫЙ удар, отнимая 55 единиц здоровья. 

Встроенные функции и интеграция

В классе LocalizationPackage встроено несколько «стандартных» функций, часть была использована в примере выше:

  • Plural(int, var1, var2, …, varN) — встраивание формы слова в зависимости от числа, данный метод уникален для каждой культуры и должен быть переопределен. В частности, в русском языке есть три формы числа (например: «1 единица», «2 единицы», «8 единиц»).
  • Random(var1, var2, …, varN) — выбор случайного значения среди заданных.
  • Upper(string) — приведение к верхнему регистру.
  • Lower(string) — приведение к нижнему регистру.
  • UpperF(string) — приведение к верхнему регистру только первой буквы («словечко» => «Словечко»).
  • LowerF(string) — приведение к нижнему регистру только первой буквы.

Если вам требуется добавить новые функции, сделать это можно двумя способами.

  1. В переопределенном классе пакета можно объявить новые функции и пометить их атрибутом [Function], тогда они будут автоматически включены в интерпретатор для конкретной локализации. Встроенные функции определены именно этим способом, например так выглядят функции Plural и Random:
    [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)];     } } 

    Обратите внимание, что для функции допустимо задание списка ее псевдонимов (для краткой записи), например Plural может вызываться как через основное имя (Plural), так и через псевдоним (P), при этом регистр в названиях функций не имеет значения.

  2. Интеграция собственных функций, для этого используется метод InjectFormatterFunction, пример использования:
    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); 

    Результат:

    8 

    В качестве аргумента для InjectFormatterFunction может быть передан метод (MethodInfo) или делегат (в примере выше передаются делегаты).

Дополнительные возможности

Помимо основных функций, библиотека предоставляет еще два дополнения.

Отладочный режим

В Debug-версии библиотеки включена возможность не только получения локализованных строк описанными выше способами, но и их непосредственная запись:

var package = LocalizationManager.Load("ru"); package["New Key"] = "Новое значение"; package.Save(); 

В этом случае будет создана новая локализованная строка с указанным ключом и значением (либо перезаписана уже имеющаяся), а сам пакет сохранен на диск. Также в режиме отладки при попытке чтения строки с отсутствующим ключом, будет возвращено пустое значение, но при этом будет создана новая запись. Это удобно на начальном этапе разработки — нам не нужно заботится о наполнении словаря — он сам будет пополняться пустыми значениями, которые мы потом заполним данными.

В релизе функции записи недоступны, что вполне логично — промышленная программа не должна уметь пополнять свой словарь локализации.

Маппинги

Это наш десерт. Назначение — быстрая локализация форм, контролов и других сложных объектов.
Данная функция используется в демонстрационном проекте LocalizationViewer.

Приведу отрывок описания главной формы:

[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;          ...          /// <summary>         /// применяем локализацию         /// </summary>         private void Localize()         {             LocalizationMapper mapper = new LocalizationMapper();             mapper.Current = manager["ru"];             mapper.Localize(this);         }          ... } 

LocalizationMapper, позволяет локализовать любой объект, переданный ему в функции Localize, используя атрибуты [Localizable] и [LocalizableClass] на полях и свойствах локализуемого объекта (в данном случае — формы). Например, атрибут [LocalizableClass] без параметров означает, что надо локализовать свойство по умолчанию (Text), при этом будет использован автоматический ключ вида <class>.<subclass>.<field>. Для поля Text кнопки cmdExit ключ будет таким:

LocalizationViewer.frmMain.cmdExit_Text 

Заключение

Библиотека в скором времени будет опробована в одном из моих проектов, так что скорее всего будут кое-какие доработки, в основном направленные на расширение базового функционала пакетов. Следите за обновлениями на SourceForge и пишите свои комментарии и мысли по дальнейшему развитию библиотеки.

P.S.

Возможно, вы скажете, что я изобретаю велосипед. Пусть так, но изобретать велосипеды — это мое хобби…
К тому же, это намного интереснее и полезнее в плане самосовершенствования в программировании.
По этой же причине здесь не будет ссылок на литературу и прочие источники информации — все было написано с нуля.

ссылка на оригинал статьи http://habrahabr.ru/post/190556/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *