Documentation

is a tiny Scala library which provides a basic, strictly-typed support for localization. The idea is that language-dependent messages reside in subclasses of abstract classes; that is - in scala code, not in resources.

Contents of this page:

How to create localized stuff

I love strict static typization, so in my web applications I wanted to code localized messages in code, not in external text files. This is how localization resources for some web signup form can be defined using :

					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 default = EN
					}
				

So, the steps are:

  1. Define abstract class or trait T which declares all stuff you need localized as abstract properties or methods.
  2. Define companion object T extends L10N[T].
  3. In companion object:
    • Define vals with names equal to uppercase language code and values of type T - for every language you're going to need. No language codes are hard-coded into library, so you are free to use any.
    • Specify default language by overriding the only abstract L10N[T]'s property - default.

When you add some localized message, you may not know how to translate it to all languages your application supports. To avoid compilation errors ("object creation impossible since method ... is not defined"), you can extract implementation for default language (usually English) into base class and subclass implementations for all other language from it:

					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 is skipped, we shall translate it later.
							override val lblPassword = "Пароль:"
							// ...
						}
						// ...
						val default = EN
					}
				

The price is "override" keyword along every property/method in subclasses.

How to access what you've created

To get your SignupL10N instance for specific language:

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

That is, you call your companion object's apply() method (inherited from L10N[T]) providing it with case-insensitive language code. The appropriate object member is determined via reflection on the upper-cased language code. If appropriate is not found, default is returned.

Now, object L10N contains thread-local property currentLang. When you call apply() without arguments, that property will be used:

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

Before you initialize L10N.currentLang, it contains null and parameterless call to L10N[T].apply() returns default.

Package ru.dimgel.lib.i18n contains implicit function that calls apply() for you, so you can even omit braces in parameterless call (for current language):

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

Use by-name arguments where localized stuff is expected

All function arguments that may take localized values should be specified as by-name. Otherwise you may get incorrect behaviour:

					// Argument 'msg' is by-value.
					class Validator(msg: String => String) {
						def validate(login: String) { return Error(msg(login)) }
					}
					
					class SignupForm(l10n: SignupL10N) {
						// Because 'msg' is by-value above, L10N[T] is resolved to T at the moment
						// of form construction, not at the moment Validator.validate() is called
						// with correct current thread-local language.
						val login = new TextField(new Validator(l10n.msgLoginAlreadyExists))
					}
				

And this will be ok:

					// Argument 'msg' is by-name.
					class Validator(msg: => String => String) {
						// ...
					}
					// ...
				

Advantages and disadvantages

In commonly used external string resources, when one needs to substitute values into string, I usually saw sprintf-like formatter strings ("Login %s already exists") or strings with macros ("Login {login} already exists"). Both are rather limiting and dangerous.

My favorite example is numeric declensions. They are language specific. In English, you just append "s" when talking of multiple objects: "1 dog", "2 dogs". In Russian, rules are much more complex: there are three different suffixes are used depending on... well, depending. :-) Let's just say it's impossible to write "123 dog(s)" like it's usually done in English (which looks lame anyway). Here how I do it with :

					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, " ", "письмо", "письма", "писем")
						}
					}
				

Classes util.English and util.Russian are part of the library.

What I want to say is that with Scala code you have no restrictions. You can assert on method arguments, for example. Besides, you are protected from mistypings by Scala compiler.

Disadvantages are:

  1. Recompilation after editing message text takes far longer than just re-packaging jar/war.
  2. Those are usually not programmers who provide translations. Give them access to code?
  3. Not applicable for editable resources (stored in the database, for example).

Although I can propose a kind of solution for first two problems - to split localization stuff into separate subproject, that solution has its own bad sides. As of third problem, I didn't mean to cover that use case in this library for now.