Pull to refresh

Comments 62

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

Если я правильно понял, поиск ресурса осуществлялся по строке-ключу?
В си нет классов) Можно эмулировать для реализации ООП, но как у вас точно не выйдет)
Буква дня: C = Сурово. Тогда исходники можно не искать. В C я умею, но сейчас на нём не пишу, так что решение будет уже не актуально.
Но все ваши требования есть из коробки с resx файлами же О_о

Разве что п4 может быть не очевидным — можно создавать несколько ресурсов и использовать их под разные цели.
Распишите поподробнее, пожалуйста. Можно даже с примерами кода. Те требования, которые прописаны в начале статьи, — это как раз то, чего я в ресурсных файлах не обнаружил.
Создаем в проекте resx файл (например Strings.resx), после этого:
1. Его ресурсы доступны по сгенерированному классу Strings, например Strings.Text1 — проверка валидности на этапе компиляции, всё отлично.
2. Если хочется, можно использовать обёртки над сгенерированным классом, и тогда доступ можно делать вложенным, т.е. условно EntityRepository.Strings будет отдавать тот самый Strings, что сгенерился автоматически. Итого — EntityRepository.Strings.Text1
3. Локализация — сложный вопрос. Что вам мешает использовать форматирование строк, чтобы менять порядок слов в разных локализациях?
4. Используйте несколько файлов ресурсов. По отдельному файлу под сборку, под сущность, под диалог — всё на ваше усмотрение.
5. Добавление языка простейшее — на каждый resx файл нужно создать $код языка.resx файл и просто перевести всё его содержимое. Различных утилит для этого хватает.
  1. Strings.Text1 — да, Strings.Messages.Errors.SuccessfullException — нет.
  2. С этим согласен.
  3. Пример из статьи с существительными и прилагательными. Когда в строке нужно вывести {моё}{собака}, то на русском это будет звучать как моя собака, на белорусском мой сабака (ещё одно такое прикольное слово — «шинель», у белорусов тоже мужского рода), в английском my dog (без указания рода), а на латыни et canem meum (и род есть, и слова переставлены). То есть, нужно хранить не строку формата + строку подстановки, а сложную структуру, в которой встречаются не только строки.
  4. В одном из проектов в таком случае у меня вышло бы несколько сотен файлов. При необходимости внесения небольших правок я не хочу искать, что и в каком файле лежит.
  5. Блокнот. Меня устраивает возможность редактирования блокнотом. Я точно уверен, что текстовый редактор такого уровня точно установлен у всех пользователей, независимо от платформы.
1. Но второй пункт это покрывает. Не совсем из коробки, но сделать не сложно.
3. А это вообще нельзя хранить, ибо никогда не знаешь заранее, как оно выйдет в другом языке. Сюда же числительные — 1 день, 2 дня, 10 дней, но 1 day vs any days. Тут решения чисто ресурсами не зайдут никогда.
4. А ваш случай ничем это не облегчает. Искать всё равно приходится, для облегчения поиска надо правильно по смыслу разделять ресурсы.
5. resx файлы это xml простейший, если сильно хочется — можно и в блокноте. Сочувствую локализатору, которому придётся любой формат переводить в блокноте.
1, 4 и (2). А зачем писать обёртку, если её можно не писать? Класс Language и играет роль такой обёртки, только ему для работы не требуется вообще никакой дополнительной логики — только свойства со значениями.

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

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

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

$Тип.$код языка.resx - например, Strings.ru.resx
Всё - больше ничего делать не надо, просто в коде вместо "строки" вызываем Strings.Name

А дальше в работу переводчику... Который в гробу видел ваши IDE и компиляторы.

Обычно есть готовые тулы, которые существующие форматы выгружают куда-надо. И загружают результат обратно.

Так что переводчикам и не нужно уметь в IDE и компиляторы.

В чём проблема перевести только то, что в value?

<data name="CopyStopwatch" xml:space="preserve">
    <value>Click middle mouse button to copy stopwatch value</value>
  </data>
  <data name="Demo" xml:space="preserve">
    <value>Example result of selected method</value>
  </data>
  <data name="DemoBilinear" xml:space="preserve">
    <value>Example result of bilinear interpolation</value>
  </data>
  <data name="Fast" xml:space="preserve">
    <value>Fast</value>
  </data>

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


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


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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here
Кстати, GWT для командной разработки очень хорошо, это же по сути просто Java, компилятор ловит множество проблем, особенно после рефакторинга. И, кстати, view models отлично можно переиспользовать для Android приложения.
UFO just landed and posted this here
Согласен, выглядит очень похоже. Сколько-нибудь значительных отличий я вижу два:
  1. Я не привязываюсь к ini-файлам. IMHO, формат данных каждый разработчик может выбрать сам с учётом своей аудитории.
  2. Я стараюсь не лезть в рефлексию, а всё делать через непосредственные вызовы свойств интерфейсов.
кто-то невнимательный написал GetResource(«asdf») вместо GetResource(«assf»)

Так и пишите Resources.assf. В Microsoft-е любят кодогенерацию и это то место, где она к месту. В вашем же решении придётся всё править вручную. А писать «Ui_PromtDialog_AdditionalQuestion» или «Ui.PromtDialog.AdditionalQuestion» — дело вкуса, за исключением того, что вот во втором случае нужно позаботится о возможном NullReferenceException.
А каким образом Вы в имя ресурса точки запихнёте? Если так сделать, то вместо «Resources.a.b.c» автоматически сгенерируется «Resources.a_b_c». Я не хочу воевать с компилятором, наблюдая в списке автодополнения пару сотен строк, выбранных из плоского списка — я хочу, чтобы с каждым введённым мной идентификатором список подсказок сужался.
Разве автодополнение не работает по заглавным буквам? По крайней мере, с решарпером оно точно работает. В конце концов, можно создать несколько ресурсных файлов. Не понятно ещё, почему у вас задача обращения к ресурсам из кода стояла так остро, что пришлось строить велосипед. Т.е. иногда, конечно бывает надо. Но Winforms сам биндится к ресурсам — ничего писать не надо. А WPF, я не уверен, что он и с вашим решением предоставит в разметке какой-то удобоваримый интелисенс.

Ещё один недостаток — это то, что что-то вроде ResourceDisplayNameAttribute уже не напишешь. Я сам иногда люблю строить велосипеды, но мой вам совет — одумайтесь.
Принцип модульности говорит, что пихать всё в один файл — плохо. Процедуры обработки строк должны лежать в StringRoutines.code, процедуры работы с датами в DateRoutines.code, слой доступа к данным в DataMiner.code и так далее. Поэтому один файл с ресурсами — это плохо, это — наваленная куча, а от наваленных куч всегда плохо пахнет. Если бы ресурсные файлы можно было организовывать в иерархические структуры типа Resources: { StringResources: { ErrorMessages, UiTooltips }, BitmapResources }, а хранить можно было не только строки+картинки, а объекты — то и смысла в этой статье не было бы. Но до тех пор, пока ресурсный файл представляет собой помойку строк++, я буду утверждать, что для локализации ресурсные файлы не подходят.
Так и разнесите ресурсные файлы по папкам и соответствующим неймспейсам. Будет вам Resources.StringResources.ErrorMessages
Чтобы было понятно, сформулирую техническое противоречие:
данные должны быть структурированы, чтобы было удобно к ним обращаться из кода;
и данные не должны быть структурированы, чтобы не ползать по папкам в поисках нужного файла.
Предложенное решение — хранить структурированные данные в одном структурированном файле (XML, JSON, etc).
Ещё одна проблема с ресурсами, разбросанными по папкам — возможность добавления данных только в листовые элементы (файлы), но не в узлы дерева (папки). То есть, я не могу сделать одновременно свойство типа string и дочерние сущности типа объект. Какое решение можете предложить для этой проблемы?
XLIFF 2.0 и куча редакторов к нему, хотябы от MS: Multilingual App Toolkit (автоперевод строк и т.д.).
> «Пользуйтесь на здоровье.»
p.s. код лучше на гитхаб выкладывать
XLIFF и MS: Multilingual App Toolkit посмотрю, спасибо. Про гитхаб уже понял — постараюсь уже сегодня перезалить.

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

Один раз — не быдлокодер =). Я думал прямо на habrastorage залить, а этот гад, оказывается, только картинки ест. Потому решил кинуть быстро куда-нибудь, что под руку подвернулось. Сегодня перелью на гитхаб по многочисленным просьбам трудящихся.
простите, но я совсем не знаю о мире .Net и C#, но неужели туда никто не принес GNU Gettext?
Если принёс, и вы на него смотрели, можно коротко «что там не так»?

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

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

Вы сами хотя бы раз 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 ""

Это же то, что действительно нужно — возле строки в момент перевода знать, где она используется?
Gettext я не использовал. Всё-таки это стороннее решение, которое надо ещё решить использовать. А иерархия нужна, потому что перевести фразу правильно можно только тогда, когда знаешь контекст, в котором она употребляется. Если ты видишь, в каком месте и какой формы она она прицеплена (есть подробный иерархический ключ), то и перевести просто. Если просто видишь фразу, висящую в вакууме, то и при переводе сделать максимум получится сферического коня. Пример из текущего проекта с нынешней работы: 6 строк, лежащих в разных местах, в английской локализации имеют вид «Successfull». В японской локали все шесть строк разные, хоть и говорят об успешном завершении.
Подскажите как локализовать то, что аннотируется через DisplayName/Description/(Category).
По-простому не выходит.
У меня такая задача не стояла, но могу рассказать, с чего бы я начал свои поиски решения (в рамках указанного в статье подхода). Все три указанных типа имеют виртуальные методы, которые могут быть перегружены в наследнике, который Вы создадите.
class DisplayNameExtendedAttribute : DisplayNameAttribute
{
	public override string DisplayName
	{
		get { return %language%.%Member%; }
	}
}

Откуда в данном примере брать язык — вопрос №1. (Сидящий на левом плече чёрт уже кричит в ухо: «Singleto-o-on!») Второй вопрос — как получить нужный член. Очевидная идея — передать туда base.DisplayName — не слишком сочетается с требованием не использовать строки. Дальше надо думать.
Есть ещё один прикол. Если стоит задача переключения языка на лету, то тут наступает огромное разочарование. Приведу несколько кусочков кода компонента 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;
    }
  }
}


Если вкратце, то у однажды загруженных свойств описание кэшируется и при смене языка остаётся навсегда тем же. С категорией то же самое. А вот показываемое имя изменяется.
Короче, боль.
Хаха… При просмотре исходника наткнулся на поле metadataVersion внутри MemberDescriptor. Раскрутка его использования привела к статическому методу TypeDescriptor.Refresh(Type).
Надо будет попробовать.
А пробовали реализовать своему типу custom TypeDescriptor с переопределённым методом GetAttributes? То есть, чтобы Attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute возвращал атрибут с локализованным значением.
Вопрос не в просто локализации, а в переключении языков на лету.
В кэшировании значений строк в недрах WinForms в случае декларативного описания, как выше. Никогда не знаешь, где ещё грабли лежат.
У класса TypeDescriptor есть метод Refresh. Есть возможность вызвать его при переключении языка?
Не прошло и полгода, вернулся наконец к этому вопросу.
Вызов TypeDescriptor.Refresh (и любой его перегрузки) не привело к ожидаемому эффекту: кэш со строками сброшен не был. Печаль :(
Реализовал такой подход в проекте, однако есть один минус:
Если все хранить в одном файле (аля rus.xml) то добавление новых значений существенно усложняют жизнь локализаторам.
В итоге в новом проекте полностью поменял структуру.
Теперь каждая строка это отдельный ресурс который знает о себе какие либо специфичесике особенности касаемые каждого языка (выбираем от чего наследует) а так же на какие языки он переведен.
Была написана утилита которая делала выборку из доступных проекту ресурсов и выгружала все это в (Xls, doc, xml, json ) и парсила обратно.
В чем плюс: нам нужно перевести только то что не перевили для нового апдейта, или то что «будем преводить на португальский потом, сейчас переводчик заболел и апдейт будет без локализации на португальский нововведений», задаем параметры выгрузки, утилита сама формирует нужную доку с нужным языком по нужным изменениям с нужными ремарками. Шлем это переводчику, обратно переведенную доку парсим в проект и применяем изменения.
Плюс всего этого подхода еще и в том, что для любой svn/git видим изменения и историю по каждой строке.
Юзайте отдельный физический ресурс аля tag_id_rus.res для каждого лейбла проэкта. Пишите инструменты автоматизации.
Если все хранить в одном файле (аля rus.xml) то добавление новых значений существенно усложняют жизнь локализаторам.
Чтобы не усложнять переводчикам жизнь, можно отправлять им Diff текущих изменений, чтобы знали, что и где править. Я уже говорил выше в других комментариях, что предпочитаю всё хранить в единственном файле, а не размазывать локализованные ресурсы по дочерним папкам. Предложенный способ локализации не универсален, а потому следовать ему или нет — вопрос требований, стоящих при разработке.
Во всех известных мне системах локализации, если локализованная строка не найдена, остаётся стандартная (английская, или текст, вшитый в приложение). Часто юзеры берут файл перевода от предыдущего релиза программы и мирятся с тем, что пара новых строк не переведена.

В вашем случае, если в класс добавилось поле, а в сериализованном файле его нет, свойство будет иметь значение null?
Да, такая проблема есть. Впрочем, это не намного хуже, чем «текст, вшитый в приложение», который чаще всего является нечитаемой аббревиатурой с кучей подчёркиваний.
1. Аббревиатура лучше пустого места
2. Как правило, если локализованный ресурс не найден, ресурс подгружается из дефолтной локали (английской, например), которую поддерживает разработчик приложения и гарантированно обновляет с каждым релизом (т.к. сам на ней тестирует)
1. Согласен.
2. Дефолтная локаль и у меня есть. То есть, при выходе новой версии можно переключиться на язык по умолчанию, посмотреть на нужную строку в программе, скормить её автопереводчику и записать в локализацию самому. Способ костыльный, зато позволяет не простаивать в ожидании официальной локализации.
Sign up to leave a comment.

Articles