Документация

- крохотная Scala-библиотека, предоставляющая базовую, строготипизированую поддержку локализации. Идея в том, что тексты сообщений для конкретных языков находятся в подклассах абстрактных классов; то есть в scala-коде, а не в ресурсах.

Содержание этой страницы:

Как создавать локализованные сообщения и т.п.

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

					package myapp
					import ru.dimgel.lib.i18n._
					
					trait SignupL10N {
						def lblLogin: String
						def lblPassword: String
						def msgLoginAlreadyExists(login: String): String
						// ...
					}
					
					object SignupL10N extends L10N[SignupL10N] {
						val EN = new SignupL10N {
							val lblLogin = "Login:"
							val lblPassword = "Password:"
							def msgLoginAlreadyExists(login: String) = "Login " + login + " is already exists."
							// ...
						}
						val RU = new SignupL10N {
							val lblLogin = "Логин:"
							val lblPassword = "Пароль:"
							def msgLoginAlreadyExists(login: String) = "Логин " + login + " уже существует."
							// ...
						}
						// ...
						val default = EN
					}
				

Итак, шаги следующие:

  1. Объявите абстрактный класс или trait T, объявляющий все требующие локализации ресурсы в виде абстрактных свойств или методов.
  2. Создайте объект-компаньон object T extends L10N[T].
  3. В объекте-компаньоне:
    • Объявите val-ы с именами, равными кодам языков в верхнем регистре и значениями типа T - для каждого языка, который вам потребуется. Коды языков в библиотеку не захардкожены, вы можете использовать любые.
    • Укажите язык по умолчанию, задав значение для единственного абстрактного свойства класса L10N[T] - default.

Когда вы добавляете какое-либо локализованное сообщение, вы можете не знать, как его перевести на все поддерживаемые вашим приложением языки. Чтобы избежать ошибок компиляции ("object creation impossible since method ... is not defined"), можно вынести реализацию для языка по умолчанию (как правило, английского) в базовый класс и наследовать остальные языки от него:

					trait SignupL10N {
						def lblLogin: String
						def lblPassword: String
						// ...
					}
					class SignupL10N_EN extends SignupL10N {
						val lblLogin = "Login:"
						val lblPassword = "Password:"
						// ...
					}
					object SignupL10N extends L10N[SignupL10N] {
						val EN = new SignupL10N_EN
						val RU = new SignupL10N_EN {
							// lblLogin пропустили, потом переведём.
							val lblPassword = "Пароль:"
							// ...
						}
						// ...
						val default = EN
					}
				

Цена - необходимость писать "override" у каждого свойства/метода в подклассах.

Как обращаться к локализованным ресурсам

Чтобы получить экземпляр вашего класса SignupL10N для конкретного кода языка:

					val msg = SignupL10N("en").msgLoginAlreadyExists("dimgel")
				

То есть, вы вызываете метод apply() своего объекта-компаньона (унаследован из L10N[T]), передавая ему код языка в любом регистре. Подходящий член объекта определяется через рефлексию по коду языка в верхнем регистре. Если подходящих не найдено, возвращается default.

Далее, object L10N содержит thread-local свойство currentLang. Когда вы вызываете apply() без аргументов, будет использовано это свойство:

					L10N.currentLang = "en"
					val msg = SignupL10N().msgLoginAlreadyExists("dimgel")
				

Перед первой инициализацией свойство L10N.currentLang содержит null, и вызов L10N[T].apply() без параметров возвращает default.

Package ru.dimgel.lib.i18n содержит implicit-функцию, вызывающую apply(), так что вы можете даже не указывать скобки при вызове без параметров (для текущего языка):

					import ru.dimgel.lib.i18n._
					L10N.currentLang = "en"
					val msg = SignupL10N.msgLoginAlreadyExists("dimgel")
				

Используйте by-name аргументы где ожидаются локализованные данные

Аргументы функций, которым могут передаваться локализованные значения, следует объявлять как by-name. В противном случае вы можете получить некорректное поведение:

					// Аргумент 'msg' объявлен как by-value.
					class Validator(msg: String => String) {
						def validate(login: String) { return Error(msg(login)) }
					}
					
					class SignupForm(l10n: SignupL10N) {
						// Поскольку 'msg' объявлен выше как by-value, L10N[T] разрешается в T 
						// в момент создания формы, а не в момент вызова Validator.validate() 
						// с корректно установленным текущим thread-local кодом языка.
						val login = new TextField(new Validator(l10n.msgLoginAlreadyExists))
					}
				

А вот так будет нормально:

					// Аргумент 'msg' объявлен как by-name.
					class Validator(msg: => String => String) {
						// ...
					}
					// ...
				

Достоинства и недостатки

В общеиспользуемых внешних строковых ресурсах, для подстановки значении в строку обычно используются строки форматирования в духе sprintf ("Login %s already exists") или же строки с макросами ("Login {login} already exists"). Оба варианта довольно убоги и опасны.

Мой любимый пример - склонения с числами. Правила зависят от языка. В английском, вы просто добавляете "s" в конец слова во множественном числе: "1 dog", "2 dogs". В русском правила гораздо сложнее: есть три варианта окончаний. Написать что-то типа английского "123 dog(s)" уже не выйдет (да и в английском оно выглядит по-ламерски). Вот так я это решаю с помощью :

					import ru.dimgel.i18n._
					
					trait SenderL10N {
						def progress(n: Int): String
					}
					object SenderL10N extends L10N[SenderL10N] {
						val EN = new SenderL10N {
							def progress(n: Int) = 
								"Sent " + n + util.English.numericDeclension(n, " letter")
						}
						val RU = new SenderL10N {
							def progress(n: Int) =
								util.Russian.numericDeclension(n, "Отправлен", "о", "ы", "о") + 
								n + 
								util.Russian.numericDeclension(n, " ", "письмо", "письма", "писем")
						}
					}
				

Классы util.English и util.Russian входят в библиотеку.

Я хочу сказать, что в Scala-коде у вас нет ограничений. Например, вы можете добавить assert-ы на параметры. И кроме того, вы надёжно защищены от опечаток компилятором Scala.

Недостатки:

  1. Перекомпиляция после редактирования текстов сообщений занимает гораздо больше времени, чем просто переархивация jar/war.
  2. Переводы на другие языки делаются обычно не программистами. Давать переводчикам доступ к коду?
  3. Не поддерживаются редактируемые ресурсы (например, хранимые в базе данных).

Хотя я и могу предложить в качестве решения первых двух проблем выделять локализации в отдельный подпроект, это решение имеет свои недостатки. Что же до третьей проблемы, то я и не собирался покрывать этот use case в данной библиотеке... пока что.