Всем добрый день. Я хочу представить на суд общественности (ещё один) простой способ сделать локализацию приложений. Стандартный механизм с ресурсными сборками меня не устраивает по следующим причинам:

  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} или <ObjectDataProvider> (WPF) и простой привязки данных.

  5. Создаём в папке с нашим приложением папку «Languages», «Localizations» (или как-нибудь в этом роде) и делаем в ней один или несколько файлов, в которых будут находиться сериализованные выбранным способом языки.

  6. При необходимости локализации более сложных вещей (картинок, например) язык будет хранить относительный путь к файлу ресурса. Сам файл в таком случае будет находиться в подпапке папки «Languages/Localizations».

  7. При загрузке приложения с помощью стандартного десериализатора подгружаются языки. Текущий выбранный язык определяется из сохранённых настроек, выбирается в выпадающем списке диалога на старте приложения (например, если приложение запускается впервые и в конфиге ничего нет), или выбирается автоматически из имеющихся на основании CultureInfo.CurrentCulture. Выбранный язык можно сохранить в любом объекте, доступ к которому (неважно каким образом — хоть через singleton, хоть через dependency injection,… — вписать предпочтительный вариант) может быть получен из тех мест, где требуются локализованные ресурсы.

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

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

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


  1. Wedmer
    02.03.2017 23:09
    +1

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


    1. courage_andrey
      03.03.2017 08:43

      Согласен, я тоже на 100% уверен, что кто-то ещё уже делал так (или почти так). Если не секрет, какое решение использовали для локализации у себя Вы? Код можно глянуть?


      1. Wedmer
        03.03.2017 14:59

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


        1. courage_andrey
          03.03.2017 15:04

          Если я правильно понял, поиск ресурса осуществлялся по строке-ключу?


          1. Wedmer
            03.03.2017 15:24

            В си нет классов) Можно эмулировать для реализации ООП, но как у вас точно не выйдет)


            1. courage_andrey
              03.03.2017 15:36

              Буква дня: C = Сурово. Тогда исходники можно не искать. В C я умею, но сейчас на нём не пишу, так что решение будет уже не актуально.


  1. MonkAlex
    02.03.2017 23:25
    +4

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

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


    1. courage_andrey
      03.03.2017 08:45

      Распишите поподробнее, пожалуйста. Можно даже с примерами кода. Те требования, которые прописаны в начале статьи, — это как раз то, чего я в ресурсных файлах не обнаружил.


      1. MonkAlex
        03.03.2017 09:25
        +4

        Создаем в проекте resx файл (например Strings.resx), после этого:
        1. Его ресурсы доступны по сгенерированному классу Strings, например Strings.Text1 — проверка валидности на этапе компиляции, всё отлично.
        2. Если хочется, можно использовать обёртки над сгенерированным классом, и тогда доступ можно делать вложенным, т.е. условно EntityRepository.Strings будет отдавать тот самый Strings, что сгенерился автоматически. Итого — EntityRepository.Strings.Text1
        3. Локализация — сложный вопрос. Что вам мешает использовать форматирование строк, чтобы менять порядок слов в разных локализациях?
        4. Используйте несколько файлов ресурсов. По отдельному файлу под сборку, под сущность, под диалог — всё на ваше усмотрение.
        5. Добавление языка простейшее — на каждый resx файл нужно создать $код языка.resx файл и просто перевести всё его содержимое. Различных утилит для этого хватает.


        1. courage_andrey
          03.03.2017 09:57
          +1

          1. Strings.Text1 — да, Strings.Messages.Errors.SuccessfullException — нет.
          2. С этим согласен.
          3. Пример из статьи с существительными и прилагательными. Когда в строке нужно вывести {моё}{собака}, то на русском это будет звучать как моя собака, на белорусском мой сабака (ещё одно такое прикольное слово — «шинель», у белорусов тоже мужского рода), в английском my dog (без указания рода), а на латыни et canem meum (и род есть, и слова переставлены). То есть, нужно хранить не строку формата + строку подстановки, а сложную структуру, в которой встречаются не только строки.
          4. В одном из проектов в таком случае у меня вышло бы несколько сотен файлов. При необходимости внесения небольших правок я не хочу искать, что и в каком файле лежит.
          5. Блокнот. Меня устраивает возможность редактирования блокнотом. Я точно уверен, что текстовый редактор такого уровня точно установлен у всех пользователей, независимо от платформы.


          1. MonkAlex
            03.03.2017 11:51

            1. Но второй пункт это покрывает. Не совсем из коробки, но сделать не сложно.
            3. А это вообще нельзя хранить, ибо никогда не знаешь заранее, как оно выйдет в другом языке. Сюда же числительные — 1 день, 2 дня, 10 дней, но 1 day vs any days. Тут решения чисто ресурсами не зайдут никогда.
            4. А ваш случай ничем это не облегчает. Искать всё равно приходится, для облегчения поиска надо правильно по смыслу разделять ресурсы.
            5. resx файлы это xml простейший, если сильно хочется — можно и в блокноте. Сочувствую локализатору, которому придётся любой формат переводить в блокноте.


            1. courage_andrey
              03.03.2017 12:21

              1, 4 и (2). А зачем писать обёртку, если её можно не писать? Класс Language и играет роль такой обёртки, только ему для работы не требуется вообще никакой дополнительной логики — только свойства со значениями.

              3. Не «нельзя», а «нужно». Нельзя требования под реализацию подгонять. А если есть требование хранить структурированную информацию, то надо искать способ такое реализовать. В указанном примере можно схитрить, если ввести нейтральные строки «дней осталось: {0}» — я думаю, в каждом языке найдутся такие конструкции.

              5. А в чём принципиальное отличие от редактирования resx файла?

              Я не утверждаю, что придумал идеальный способ локализации. Потому и написал в заголовке статьи «Ещё один...» И, судя по первому комментарию, не я один люблю всё упрощать.


  1. rkfg
    03.03.2017 00:22

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


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


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


    1. sleeply4cat
      03.03.2017 01:37

      А что можете про GWT сказать, стоит начинать использовать сейчас?


      1. rkfg
        03.03.2017 01:55

        Не вижу причин не попробовать хотя бы. Я сам пишу веб-приложения по работе исключительно на нём вот уже почти пять лет, и создаётся впечатление, что это действительно единственный веб-фреймворк 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 и большой командой разработчиков, но для внутренних задач, пожалуй, ничего лучше не найти.


        1. sleeply4cat
          03.03.2017 02:16

          Хм. А оно как крутится, в контейнере сервлетов или само по себе?


          1. rkfg
            03.03.2017 02:25

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


            1. sleeply4cat
              03.03.2017 04:05

              Забавненько, надо будет попробовать, когда время появится…


              1. slavap
                03.03.2017 08:51

                Кстати, GWT для командной разработки очень хорошо, это же по сути просто Java, компилятор ловит множество проблем, особенно после рефакторинга. И, кстати, view models отлично можно переиспользовать для Android приложения.


                1. sleeply4cat
                  03.03.2017 21:12

                  я не умею распараллеливать работу над программой D:
                  они просто меня не слушаются


    1. courage_andrey
      03.03.2017 08:49

      Согласен, выглядит очень похоже. Сколько-нибудь значительных отличий я вижу два:

      1. Я не привязываюсь к ini-файлам. IMHO, формат данных каждый разработчик может выбрать сам с учётом своей аудитории.
      2. Я стараюсь не лезть в рефлексию, а всё делать через непосредственные вызовы свойств интерфейсов.


  1. yarosroman
    03.03.2017 01:47
    +3

    На гитхаб выложите.


    1. courage_andrey
      03.03.2017 08:54

      ОК


    1. courage_andrey
      03.03.2017 11:58

      Перезалил, ссылка теперь ведёт на GITHUB.


  1. dkukushkin
    03.03.2017 04:39

    Всё гениальное просто.


  1. petuhov_k
    03.03.2017 05:02
    +3

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

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


    1. courage_andrey
      03.03.2017 09:02
      -3

      А каким образом Вы в имя ресурса точки запихнёте? Если так сделать, то вместо «Resources.a.b.c» автоматически сгенерируется «Resources.a_b_c». Я не хочу воевать с компилятором, наблюдая в списке автодополнения пару сотен строк, выбранных из плоского списка — я хочу, чтобы с каждым введённым мной идентификатором список подсказок сужался.


      1. petuhov_k
        03.03.2017 09:44
        +1

        Разве автодополнение не работает по заглавным буквам? По крайней мере, с решарпером оно точно работает. В конце концов, можно создать несколько ресурсных файлов. Не понятно ещё, почему у вас задача обращения к ресурсам из кода стояла так остро, что пришлось строить велосипед. Т.е. иногда, конечно бывает надо. Но Winforms сам биндится к ресурсам — ничего писать не надо. А WPF, я не уверен, что он и с вашим решением предоставит в разметке какой-то удобоваримый интелисенс.

        Ещё один недостаток — это то, что что-то вроде ResourceDisplayNameAttribute уже не напишешь. Я сам иногда люблю строить велосипеды, но мой вам совет — одумайтесь.


        1. courage_andrey
          03.03.2017 10:12
          -1

          Принцип модульности говорит, что пихать всё в один файл — плохо. Процедуры обработки строк должны лежать в StringRoutines.code, процедуры работы с датами в DateRoutines.code, слой доступа к данным в DataMiner.code и так далее. Поэтому один файл с ресурсами — это плохо, это — наваленная куча, а от наваленных куч всегда плохо пахнет. Если бы ресурсные файлы можно было организовывать в иерархические структуры типа Resources: { StringResources: { ErrorMessages, UiTooltips }, BitmapResources }, а хранить можно было не только строки+картинки, а объекты — то и смысла в этой статье не было бы. Но до тех пор, пока ресурсный файл представляет собой помойку строк++, я буду утверждать, что для локализации ресурсные файлы не подходят.


          1. petuhov_k
            03.03.2017 10:48
            +1

            Так и разнесите ресурсные файлы по папкам и соответствующим неймспейсам. Будет вам Resources.StringResources.ErrorMessages


            1. courage_andrey
              03.03.2017 11:04

              Чтобы было понятно, сформулирую техническое противоречие:
              данные должны быть структурированы, чтобы было удобно к ним обращаться из кода;
              и данные не должны быть структурированы, чтобы не ползать по папкам в поисках нужного файла.
              Предложенное решение — хранить структурированные данные в одном структурированном файле (XML, JSON, etc).
              Ещё одна проблема с ресурсами, разбросанными по папкам — возможность добавления данных только в листовые элементы (файлы), но не в узлы дерева (папки). То есть, я не могу сделать одновременно свойство типа string и дочерние сущности типа объект. Какое решение можете предложить для этой проблемы?


  1. verysimplenick
    03.03.2017 09:02

    XLIFF 2.0 и куча редакторов к нему, хотябы от MS: Multilingual App Toolkit (автоперевод строк и т.д.).
    > «Пользуйтесь на здоровье.»
    p.s. код лучше на гитхаб выкладывать


    1. courage_andrey
      03.03.2017 09:04

      XLIFF и MS: Multilingual App Toolkit посмотрю, спасибо. Про гитхаб уже понял — постараюсь уже сегодня перезалить.


  1. streetturtle
    03.03.2017 09:05
    +3

    Выкладывать код в архиве на гугл драйв… Мсье знает толк в извращениях...


    1. courage_andrey
      03.03.2017 09:07
      -2

      Один раз — не быдлокодер =). Я думал прямо на habrastorage залить, а этот гад, оказывается, только картинки ест. Потому решил кинуть быстро куда-нибудь, что под руку подвернулось. Сегодня перелью на гитхаб по многочисленным просьбам трудящихся.


    1. courage_andrey
      03.03.2017 11:58

      Перезалил, ссылка теперь ведёт на GITHUB.


  1. bormotov
    03.03.2017 12:55

    простите, но я совсем не знаю о мире .Net и C#, но неужели туда никто не принес GNU Gettext?
    Если принёс, и вы на него смотрели, можно коротко «что там не так»?

    Я пока вижу, что точно «не так» будет пп.4 ваших требований (там языковый — в какой-то мере тоже плоский список), и от себя могу добавить — там несколько другой подход не то что к локализации софта, а вообще к разработке софта, который должен уметь выводить тексты на множестве языков.

    Еще может быть неприятная проблема с лицензий библиотеки, но кажется, написать свою реализацию не должно составить большого труда.


    1. courage_andrey
      03.03.2017 13:32

      Реализация Gettext для NET существует, но наличие иерархической структуры в локализованных данных (это не только от пункт 4, это 1-4) было реально важно для тех проектов, для которых я делал локализацию. Кроме того, подключение Gettext потребует подключения дополнительных сторонних библиотек, а предложенный подход основывается на использовании только стандартных «кирпичей». Статья описывает способ локализации проекта, для которого важны перечисленные в начале статьи требования, но ни разу не золотую пулю для решения всех проблем локализации.


      1. bormotov
        03.03.2017 20:51

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

        Вы сами хотя бы раз gettext использовали? Как разработчик, как автор перевода?

        Оба примера кода в пп.2 для меня выглядят дико.

        Писать string foo = language.Ui.PromtDialog.AdditionalQuestion просто приятнее, чем string foo = Resources.GetResource(«Ui_PromtDialog_AdditionalQuestion»)


        В случае gettext это будет что-то типа
        string foo = _("Additional question raw text")
        


        Или только мне кажется, что в самом исходнике вот эти language.Ui.PromtDialog.AdditionalQuestion — это какое-то масло-масляное?

        Если файл исходника лежит в src/UI/PromptDialog.cs

        то в po-файле это будет как-то так

        #: D:src/UI/PromptDialog.cs:37
        #, csharp-format
        msgid "Additional question raw text"
        msgstr ""
        

        Это же то, что действительно нужно — возле строки в момент перевода знать, где она используется?


        1. courage_andrey
          04.03.2017 21:55
          +1

          Gettext я не использовал. Всё-таки это стороннее решение, которое надо ещё решить использовать. А иерархия нужна, потому что перевести фразу правильно можно только тогда, когда знаешь контекст, в котором она употребляется. Если ты видишь, в каком месте и какой формы она она прицеплена (есть подробный иерархический ключ), то и перевести просто. Если просто видишь фразу, висящую в вакууме, то и при переводе сделать максимум получится сферического коня. Пример из текущего проекта с нынешней работы: 6 строк, лежащих в разных местах, в английской локализации имеют вид «Successfull». В японской локали все шесть строк разные, хоть и говорят об успешном завершении.


  1. a-tk
    03.03.2017 14:05

    Подскажите как локализовать то, что аннотируется через DisplayName/Description/(Category).
    По-простому не выходит.


    1. courage_andrey
      03.03.2017 15:02

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

      class DisplayNameExtendedAttribute : DisplayNameAttribute
      {
      	public override string DisplayName
      	{
      		get { return %language%.%Member%; }
      	}
      }
      

      Откуда в данном примере брать язык — вопрос №1. (Сидящий на левом плече чёрт уже кричит в ухо: «Singleto-o-on!») Второй вопрос — как получить нужный член. Очевидная идея — передать туда base.DisplayName — не слишком сочетается с требованием не использовать строки. Дальше надо думать.


      1. a-tk
        03.03.2017 16:04

        Есть ещё один прикол. Если стоит задача переключения языка на лету, то тут наступает огромное разочарование. Приведу несколько кусочков кода компонента PropertyGrid, они хорошо иллюстрируют всю боль происходящего.

        public abstract class MemberDescriptor {
          public virtual string DisplayName {
            get {
              DisplayNameAttribute displayNameAttr = Attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute;
              if (displayNameAttr == null || displayNameAttr.IsDefaultAttribute()) {
                return displayName;
              }   
              return displayNameAttr.DisplayName;
            }
          }
        
          public virtual string Description {
            get {
              if (description == null) {
                description = ((DescriptionAttribute) Attributes[typeof(DescriptionAttribute)]).Description;
              }
              return description;
            }
          }
        
          public virtual string Category {
            get {
              if (category == null) {
                category = ((CategoryAttribute)Attributes[typeof(CategoryAttribute)]).Category;
              }
              return category;
            }
          }
        }
        


        Если вкратце, то у однажды загруженных свойств описание кэшируется и при смене языка остаётся навсегда тем же. С категорией то же самое. А вот показываемое имя изменяется.
        Короче, боль.


        1. a-tk
          03.03.2017 16:12

          Хаха… При просмотре исходника наткнулся на поле metadataVersion внутри MemberDescriptor. Раскрутка его использования привела к статическому методу TypeDescriptor.Refresh(Type).
          Надо будет попробовать.


          1. courage_andrey
            03.03.2017 16:21

            Мой следующий комментарий почти успел =)


        1. courage_andrey
          03.03.2017 16:20

          А пробовали реализовать своему типу custom TypeDescriptor с переопределённым методом GetAttributes? То есть, чтобы Attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute возвращал атрибут с локализованным значением.


          1. a-tk
            03.03.2017 16:47

            Вопрос не в просто локализации, а в переключении языков на лету.


            1. courage_andrey
              03.03.2017 16:49

              В чём принципиальная разница?


              1. a-tk
                03.03.2017 19:24

                В кэшировании значений строк в недрах WinForms в случае декларативного описания, как выше. Никогда не знаешь, где ещё грабли лежат.


                1. courage_andrey
                  04.03.2017 21:28

                  У класса TypeDescriptor есть метод Refresh. Есть возможность вызвать его при переключении языка?


  1. a3dline
    03.03.2017 16:24

    Реализовал такой подход в проекте, однако есть один минус:
    Если все хранить в одном файле (аля rus.xml) то добавление новых значений существенно усложняют жизнь локализаторам.
    В итоге в новом проекте полностью поменял структуру.
    Теперь каждая строка это отдельный ресурс который знает о себе какие либо специфичесике особенности касаемые каждого языка (выбираем от чего наследует) а так же на какие языки он переведен.
    Была написана утилита которая делала выборку из доступных проекту ресурсов и выгружала все это в (Xls, doc, xml, json ) и парсила обратно.
    В чем плюс: нам нужно перевести только то что не перевили для нового апдейта, или то что «будем преводить на португальский потом, сейчас переводчик заболел и апдейт будет без локализации на португальский нововведений», задаем параметры выгрузки, утилита сама формирует нужную доку с нужным языком по нужным изменениям с нужными ремарками. Шлем это переводчику, обратно переведенную доку парсим в проект и применяем изменения.
    Плюс всего этого подхода еще и в том, что для любой svn/git видим изменения и историю по каждой строке.
    Юзайте отдельный физический ресурс аля tag_id_rus.res для каждого лейбла проэкта. Пишите инструменты автоматизации.


    1. courage_andrey
      03.03.2017 16:33

      Если все хранить в одном файле (аля rus.xml) то добавление новых значений существенно усложняют жизнь локализаторам.
      Чтобы не усложнять переводчикам жизнь, можно отправлять им Diff текущих изменений, чтобы знали, что и где править. Я уже говорил выше в других комментариях, что предпочитаю всё хранить в единственном файле, а не размазывать локализованные ресурсы по дочерним папкам. Предложенный способ локализации не универсален, а потому следовать ему или нет — вопрос требований, стоящих при разработке.


      1. a3dline
        07.03.2017 17:27

        вполне. +за идею


  1. qw1
    05.03.2017 08:22

    Во всех известных мне системах локализации, если локализованная строка не найдена, остаётся стандартная (английская, или текст, вшитый в приложение). Часто юзеры берут файл перевода от предыдущего релиза программы и мирятся с тем, что пара новых строк не переведена.

    В вашем случае, если в класс добавилось поле, а в сериализованном файле его нет, свойство будет иметь значение null?


  1. courage_andrey
    05.03.2017 21:00

    Да, такая проблема есть. Впрочем, это не намного хуже, чем «текст, вшитый в приложение», который чаще всего является нечитаемой аббревиатурой с кучей подчёркиваний.


    1. qw1
      05.03.2017 22:05

      1. Аббревиатура лучше пустого места
      2. Как правило, если локализованный ресурс не найден, ресурс подгружается из дефолтной локали (английской, например), которую поддерживает разработчик приложения и гарантированно обновляет с каждым релизом (т.к. сам на ней тестирует)


      1. courage_andrey
        06.03.2017 08:54

        1. Согласен.
        2. Дефолтная локаль и у меня есть. То есть, при выходе новой версии можно переключиться на язык по умолчанию, посмотреть на нужную строку в программе, скормить её автопереводчику и записать в локализацию самому. Способ костыльный, зато позволяет не простаивать в ожидании официальной локализации.