Comments 62
Так или иначе спасибо за то, что делитесь своими решениями, находками, идеями.
В свое время тоже делал велосипедную систему локализации для своего приложения, чтобы не тянуть в проект лишние библиотеки.
Собственно сам экземпляр переводимого объекта при инициализации получал ссылку на нужную ячейку в таблице. При отрисовке объект использует текст по ссылке. Если в массиве нет ничего по идентификатору, по которому был инициализирован объект, то используется сам идентификатор.
Также был функционал для использования с числительными и родами.
Разве что п4 может быть не очевидным — можно создавать несколько ресурсов и использовать их под разные цели.
1. Его ресурсы доступны по сгенерированному классу Strings, например Strings.Text1 — проверка валидности на этапе компиляции, всё отлично.
2. Если хочется, можно использовать обёртки над сгенерированным классом, и тогда доступ можно делать вложенным, т.е. условно EntityRepository.Strings будет отдавать тот самый Strings, что сгенерился автоматически. Итого — EntityRepository.Strings.Text1
3. Локализация — сложный вопрос. Что вам мешает использовать форматирование строк, чтобы менять порядок слов в разных локализациях?
4. Используйте несколько файлов ресурсов. По отдельному файлу под сборку, под сущность, под диалог — всё на ваше усмотрение.
5. Добавление языка простейшее — на каждый resx файл нужно создать $код языка.resx файл и просто перевести всё его содержимое. Различных утилит для этого хватает.
- Strings.Text1 — да, Strings.Messages.Errors.SuccessfullException — нет.
- С этим согласен.
- Пример из статьи с существительными и прилагательными. Когда в строке нужно вывести {моё}{собака}, то на русском это будет звучать как моя собака, на белорусском мой сабака (ещё одно такое прикольное слово — «шинель», у белорусов тоже мужского рода), в английском my dog (без указания рода), а на латыни et canem meum (и род есть, и слова переставлены). То есть, нужно хранить не строку формата + строку подстановки, а сложную структуру, в которой встречаются не только строки.
- В одном из проектов в таком случае у меня вышло бы несколько сотен файлов. При необходимости внесения небольших правок я не хочу искать, что и в каком файле лежит.
- Блокнот. Меня устраивает возможность редактирования блокнотом. Я точно уверен, что текстовый редактор такого уровня точно установлен у всех пользователей, независимо от платформы.
3. А это вообще нельзя хранить, ибо никогда не знаешь заранее, как оно выйдет в другом языке. Сюда же числительные — 1 день, 2 дня, 10 дней, но 1 day vs any days. Тут решения чисто ресурсами не зайдут никогда.
4. А ваш случай ничем это не облегчает. Искать всё равно приходится, для облегчения поиска надо правильно по смыслу разделять ресурсы.
5. resx файлы это xml простейший, если сильно хочется — можно и в блокноте. Сочувствую локализатору, которому придётся любой формат переводить в блокноте.
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, например, несколько похожих строк, которые отличаются одним пробелом или точкой в конце, наверняка ведь можно было заменить их одной, если бы это было реализовано через тип.
Не вижу причин не попробовать хотя бы. Я сам пишу веб-приложения по работе исключительно на нём вот уже почти пять лет, и создаётся впечатление, что это действительно единственный веб-фреймворк 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 и большой командой разработчиков, но для внутренних задач, пожалуй, ничего лучше не найти.
В контейнере, но несколько моих программ работают и standalone с помощью Jetty embedded. Всё целиком находится в одном .war.
- Я не привязываюсь к ini-файлам. IMHO, формат данных каждый разработчик может выбрать сам с учётом своей аудитории.
- Я стараюсь не лезть в рефлексию, а всё делать через непосредственные вызовы свойств интерфейсов.
кто-то невнимательный написал GetResource(«asdf») вместо GetResource(«assf»)
Так и пишите Resources.assf. В Microsoft-е любят кодогенерацию и это то место, где она к месту. В вашем же решении придётся всё править вручную. А писать «Ui_PromtDialog_AdditionalQuestion» или «Ui.PromtDialog.AdditionalQuestion» — дело вкуса, за исключением того, что вот во втором случае нужно позаботится о возможном NullReferenceException.
Ещё один недостаток — это то, что что-то вроде ResourceDisplayNameAttribute уже не напишешь. Я сам иногда люблю строить велосипеды, но мой вам совет — одумайтесь.
данные должны быть структурированы, чтобы было удобно к ним обращаться из кода;
и данные не должны быть структурированы, чтобы не ползать по папкам в поисках нужного файла.
Предложенное решение — хранить структурированные данные в одном структурированном файле (XML, JSON, etc).
Ещё одна проблема с ресурсами, разбросанными по папкам — возможность добавления данных только в листовые элементы (файлы), но не в узлы дерева (папки). То есть, я не могу сделать одновременно свойство типа string и дочерние сущности типа объект. Какое решение можете предложить для этой проблемы?
> «Пользуйтесь на здоровье.»
p.s. код лучше на гитхаб выкладывать
Выкладывать код в архиве на гугл драйв… Мсье знает толк в извращениях...
Если принёс, и вы на него смотрели, можно коротко «что там не так»?
Я пока вижу, что точно «не так» будет пп.4 ваших требований (там языковый — в какой-то мере тоже плоский список), и от себя могу добавить — там несколько другой подход не то что к локализации софта, а вообще к разработке софта, который должен уметь выводить тексты на множестве языков.
Еще может быть неприятная проблема с лицензий библиотеки, но кажется, написать свою реализацию не должно составить большого труда.
Вы сами хотя бы раз 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 ""
Это же то, что действительно нужно — возле строки в момент перевода знать, где она используется?
По-простому не выходит.
class DisplayNameExtendedAttribute : DisplayNameAttribute
{
public override string DisplayName
{
get { return %language%.%Member%; }
}
}
Откуда в данном примере брать язык — вопрос №1. (Сидящий на левом плече чёрт уже кричит в ухо: «Singleto-o-on!») Второй вопрос — как получить нужный член. Очевидная идея — передать туда base.DisplayName — не слишком сочетается с требованием не использовать строки. Дальше надо думать.
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;
}
}
}
Если вкратце, то у однажды загруженных свойств описание кэшируется и при смене языка остаётся навсегда тем же. С категорией то же самое. А вот показываемое имя изменяется.
Короче, боль.
Надо будет попробовать.
Если все хранить в одном файле (аля rus.xml) то добавление новых значений существенно усложняют жизнь локализаторам.
В итоге в новом проекте полностью поменял структуру.
Теперь каждая строка это отдельный ресурс который знает о себе какие либо специфичесике особенности касаемые каждого языка (выбираем от чего наследует) а так же на какие языки он переведен.
Была написана утилита которая делала выборку из доступных проекту ресурсов и выгружала все это в (Xls, doc, xml, json ) и парсила обратно.
В чем плюс: нам нужно перевести только то что не перевили для нового апдейта, или то что «будем преводить на португальский потом, сейчас переводчик заболел и апдейт будет без локализации на португальский нововведений», задаем параметры выгрузки, утилита сама формирует нужную доку с нужным языком по нужным изменениям с нужными ремарками. Шлем это переводчику, обратно переведенную доку парсим в проект и применяем изменения.
Плюс всего этого подхода еще и в том, что для любой svn/git видим изменения и историю по каждой строке.
Юзайте отдельный физический ресурс аля tag_id_rus.res для каждого лейбла проэкта. Пишите инструменты автоматизации.
Если все хранить в одном файле (аля rus.xml) то добавление новых значений существенно усложняют жизнь локализаторам.Чтобы не усложнять переводчикам жизнь, можно отправлять им Diff текущих изменений, чтобы знали, что и где править. Я уже говорил выше в других комментариях, что предпочитаю всё хранить в единственном файле, а не размазывать локализованные ресурсы по дочерним папкам. Предложенный способ локализации не универсален, а потому следовать ему или нет — вопрос требований, стоящих при разработке.
В вашем случае, если в класс добавилось поле, а в сериализованном файле его нет, свойство будет иметь значение null?
2. Как правило, если локализованный ресурс не найден, ресурс подгружается из дефолтной локали (английской, например), которую поддерживает разработчик приложения и гарантированно обновляет с каждым релизом (т.к. сам на ней тестирует)
2. Дефолтная локаль и у меня есть. То есть, при выходе новой версии можно переключиться на язык по умолчанию, посмотреть на нужную строку в программе, скормить её автопереводчику и записать в локализацию самому. Способ костыльный, зато позволяет не простаивать в ожидании официальной локализации.
Ещё один способ локализации приложений