Ещё один способ локализации приложений

Всем добрый день. Я хочу представить на суд общественности (ещё один) простой способ сделать локализацию приложений. Стандартный механизм с ресурсными сборками меня не устраивает по следующим причинам:
  1. Получая значение локализованной строки в коде, очень хочется полагаться на всю мощь ООП и подсказки компилятора. Очень неприятно собрать проект в вечером в пятницу, а утром в субботу получить звонок от впахивающих overtime QA на тему того, что кто-то невнимательный написал GetResource («asdf») вместо GetResource («assf»), и теперь что-то падает или отображается неверно, а проект в понедельник уже сдавать в печать …
  2. (В продолжение предыдущего пункта…) Писать string foo = language.Ui.PromtDialog.AdditionalQuestion просто приятнее, чем string foo = Resources.GetResource («Ui_PromtDialog_AdditionalQuestion»). Да, в том числе и за счёт подсказок компилятора.
  3. Иногда локализовать нужно не строки, а целые объекты. Например, существительное (строка + род М/Ж/С/Мн) и прилагательное (строка М + строка Ж + строка С + строка Мн). Пихать в ресурсы сериализованную строку, а потом доставать и десериализовать каждый раз? Мсье знает толк в извращениях…
  4. Ресурсный файл — это плоский список строк, а хотелось бы, чтобы данные всё-таки имели более сложную иерархическую структуру, по которой не нужно ползать с помощью Ctrl+F.
  5. Создание нового языка должно быть настолько простым, насколько это возможно. Локализовать приложение должен быть способен человек, умеющий обращаться с компьютером и владеющий нужными языками. И ему для этого не нужны ни Visual Studio, ни возня с созданием ресурсных сборок.

Ещё одно обязательное требование — возможность простой привязки к локализации элементов UI. Желательно — одновременно и WPF, и WinForms.

Решение лежит на поверхности и по простоте способно соперничать с топором и лопатой. Следите за руками:
  1. Создаём класс с названием, например, Language, который и будет содержать в себе все локализованные ресурсы.
  2. Заполняем его свойствами типа «строка» и свойствами-объектами со строковыми свойствами («категориями»), и свойствами-объектами свойств-объектов, и… Глубину вложения выбрать по вкусу.
  3. Делаем класс Language (и все вложенные в него) сериализуемыми с помощью способа, который вызовет у почти рядового пользователя минимальное отторжение при попытке отредактировать файл с сериализованным языком. Мне больше всего импонирует XML, поэтому я выбираю, соответственно, атрибуты XmlType, XmlRoot, XmlElement, XmlAttribute. Фанаты JSON могут использовать JSON. Если под рукой есть удобный враппер для работы с ini-файлами — можно использовать и его. Всё в ваших руках.
  4. Вытягиваем язык на форму с помощью компонента BindingSource (WinForms), {x: Static} или  (WPF) и простой привязки данных.
  5. Создаём в папке с нашим приложением папку «Languages», «Localizations» (или как-нибудь в этом роде) и делаем в ней один или несколько файлов, в которых будут находиться сериализованные выбранным способом языки.
  6. При необходимости локализации более сложных вещей (картинок, например) язык будет хранить относительный путь к файлу ресурса. Сам файл в таком случае будет находиться в подпапке папки «Languages/Localizations».
  7. При загрузке приложения с помощью стандартного десериализатора подгружаются языки. Текущий выбранный язык определяется из сохранённых настроек, выбирается в выпадающем списке диалога на старте приложения (например, если приложение запускается впервые и в конфиге ничего нет), или выбирается автоматически из имеющихся на основании CultureInfo.CurrentCulture. Выбранный язык можно сохранить в любом объекте, доступ к которому (неважно каким образом — хоть через singleton, хоть через dependency injection, … — вписать предпочтительный вариант) может быть получен из тех мест, где требуются локализованные ресурсы.

Любому, кто захочет сделать новую локаль, нужно будет просто скопировать файл с удобным языком в папке «Languages/Localizations» и перевести имеющиеся в нём строки.

Пользуйтесь на здоровье. Код примера доступен для скачивания здесь. Написан он, очевидно, впопыхах, а потому несёт на себе печать непродуманного дизайна: например, языки подгружаются в коде главной формы, а не метода Main. Ну, а за способ, которым язык привязан к форме в примере Wpf, можно и пальцы пассатижами обжать. Зато — работает на 100%. Рад буду предложениям по улучшению этого метода.

Комментарии (11)

  • 2 марта 2017 в 23:09

    0

    Может я и не прав, но очень многие уже писали подобные велосипеды на разных языках, правда большинство их не опубликовывало.
    Так или иначе спасибо за то, что делитесь своими решениями, находками, идеями.
    В свое время тоже делал велосипедную систему локализации для своего приложения, чтобы не тянуть в проект лишние библиотеки.
  • 2 марта 2017 в 23:25

    +1

    Но все ваши требования есть из коробки с resx файлами же О_о

    Разве что п4 может быть не очевидным — можно создавать несколько ресурсов и использовать их под разные цели.

  • 3 марта 2017 в 00:22

    0

    Я немного не в тему, потому что про Java, но эти языки очень похожи. В GWT похожий способ, правда, не совсем иерархический. Пишется интерфейс, расширяющий пустой интерфейс-маркер из состава GWT, его методы возвращают String, а в параметры можно передавать аргументы строки. Сама строка описывается через аннотацию @DefaultMessage (в дефолтной локали) + в ini-файлах, которые можно сгенерировать через инструментарий, также можно задать плюрализацию и дополнительные произвольные аргументы, например, пол пользователя. К сожалению, в .ini уже статически не проверить соответствие ключа имени метода в интерфейсе, так что опечатки всё равно не исключены полностью, но возможно, есть какой-то способ для конкретных IDE.


    Далее в коде инстанциируется этот интерфейс через GWT.create (под капотом создаётся прокси-объект, через рефлексию вытаскивающий нужную строку), и можно у такого объекта вызывать методы, получая в ответ локализованные строки. Локаль можно задавать разными способами, от GET-параметра до кук и ручной установки.


    Мне такие способы тоже нравятся за проверку правильности ключа, но кроме того, интерфейсы могут расширять друг друга. Например, так я сделал интерфейс CommonMessages со строками, которыми постоянно везде пользуюсь (типа Ok, Cancel, Print, Yes, No и т.д.) и дальше наследуюсь уже от него. На уровне языка получается прозрачный и проверяемый на корректность доступ к этим сообщениям, что позволяет вынести такую общую локализацию в отдельную библиотеку и не дублировать её в каждом проекте. Очень жаль, что стандартная техника локализации во многих фреймворках, языках и платформах делается через обычные строки, а не через систему типов. Неоднократно встречал в локализации проектов на transifex, например, несколько похожих строк, которые отличаются одним пробелом или точкой в конце, наверняка ведь можно было заменить их одной, если бы это было реализовано через тип.

    • 3 марта 2017 в 01:37

      0

      А что можете про GWT сказать, стоит начинать использовать сейчас?
      • 3 марта 2017 в 01:55

        0

        Не вижу причин не попробовать хотя бы. Я сам пишу веб-приложения по работе исключительно на нём вот уже почти пять лет, и создаётся впечатление, что это действительно единственный веб-фреймворк done right, особенно, если любовь к статической типизации и десктопо-подобному софту выше, чем стремление освоить очередной JS-фреймворк. Я, скажем так, не фанат JS и DOM.


        GWT позволяет писать сервер и клиент на одном и том же языке, не переключая контекст в мозгу, это хорошо, если в команде мало людей или ты вообще один (как и было в моём случае). Не могу сказать насчёт дизайна/вёрстки/изысков в области UI/UX, мы используем GWT Bootstrap, и в принципе, всё работает пристойно. Выглядит, наверно, победнее, чем сейчас принято, но всё это внутренний софт для разнообразного учёта и не только. Возможно, если бы стояла такая цель, можно было бы сделать круто и красиво, было б желание — никто не запрещает использовать CSS, HTML, native JS вместе с GWT, он отлично со всем этим делом интеропится, и я кое-где использовал стилизацию и JSNI.


        Основные плюсы — единообразие структур данных и, порой, даже части кода между сервером и клиентом, т.к. это физически один и тот же класс, один и тот же код, просто для клиента он транспилируется в JS. Отладка в Eclipse тоже прозрачная между клиентом и сервером (нужен плагин SDBG и Chrome), т.е. можно ставить брейкпоинты в клиентском и серверном коде, трейсить этот код и смотреть переменные, как будто всё написано на Java. Также можно в клиентских исключениях получать трейсбэк с номерами строк в Java-коде.


        Вся сериализация и проверки на безопасность уже встроены, можно подцеплять Hibernate и Dozer (для маппинга, чтобы сериализатор не спотыкался на ленивой загрузке), Shiro для авторизации и вперёд. Многие, похоже, используют Spring, но мне он как-то не требовался. Если какая-то структура в БД меняется, это автоматически доступно на клиенте, всё статически типизировано, т.е. веб-приложение является цельным, а не разделено на независимые фронт и бэк. Отсюда все плюсы-минусы, конечно. Скорее всего, я бы не стал делать на GWT какой-то публичный сервис с внешним API и большой командой разработчиков, но для внутренних задач, пожалуй, ничего лучше не найти.

        • 3 марта 2017 в 02:16

          0

          Хм. А оно как крутится, в контейнере сервлетов или само по себе?
          • 3 марта 2017 в 02:25

            0

            В контейнере, но несколько моих программ работают и standalone с помощью Jetty embedded. Всё целиком находится в одном .war.

            • 3 марта 2017 в 04:05

              0

              Забавненько, надо будет попробовать, когда время появится…
  • 3 марта 2017 в 01:47

    0

    На гитхаб выложите.
  • 3 марта 2017 в 04:39

    0

    Всё гениальное просто.
  • 3 марта 2017 в 05:02

    0

    кто-то невнимательный написал GetResource («asdf») вместо GetResource («assf»)

    Так и пишите Resources.assf. В Microsoft-е любят кодогенерацию и это то место, где она к месту. В вашем же решении придётся всё править вручную. А писать «Ui_PromtDialog_AdditionalQuestion» или «Ui.PromtDialog.AdditionalQuestion» — дело вкуса, за исключением того, что вот во втором случае нужно позаботится о возможном NullReferenceException.

© Habrahabr.ru