Recently, I went through the exercise of externalizing the strings on a Discord bot, pawa, so that I can provide different translations. Since this is a JVM code base, the preferred path is to use a ResourceBundle
. This would allow me to have different Properties files for each of the languages that the application would support, and as long as these Properties files are located in the classpath, the getBundle
method will find the correct one to load.
In a contrived example, this is how it would work:
fun main() {
// Default Bundle
var bundle = ResourceBundle.getBundle("strings")
var greeting = bundle.getString("greeting")
println("greeting = $greeting")
//=> greeting = hello
// Spanish Bundle
bundle = ResourceBundle.getBundle("strings", Locale("es"))
greeting = bundle.getString("greeting")
println("greeting = $greeting")
//=> greeting = hola
}
You can use built in locales such as Locale.CHINESE
or you can specify your own as I did in the example above. The only requirement is that you stick to the ISO-639 language codes. While the Locale constructor will accept 2-letter codes, it’s best to use the 3-letter code as it’s less ambiguous.
Incorrect Indonesian Language Code
When I was adding support for the Indonesian language, I made the mistake of using the language code id
code. I structured my resources as follows:
❯ exa -T src/main/resources
src/main/resources
├── strings.properties
├── strings_es.properties
└── strings_id.properties
I did that because if you go to the Indonesian language wikipedia page, it that the code is
id
.
And this is the code snippet with the result:
// Indonesian Bundle
bundle = ResourceBundle.getBundle("strings", Locale("id"))
greeting = bundle.getString("greeting")
println("greeting = $greeting")
//=> greeting = hello
🤔 hmmm, the result should’ve been greeting = halo
, however that’s not what I was seeing here.
This was confusing because, the locale object was correctly printing out the right language:
Locale("id").displayName //=> Indonesian
So what was going on?
Read the Docs
In the Locale
documentation there’s a line that talks about deprecated ISO codes:
Deprecated ISO language codes “iw”, “ji”, and “in” are converted to “he”, “yi”, and “id”, respectively.
Which appears in the private static method Locale.convertOldISOCodes
private static String convertOldISOCodes(String language) {
// we accept both the old and the new ISO codes for the languages whose ISO
// codes have changed, but we always store the OLD code, for backward compatibility
language = LocaleUtils.toLowerString(language).intern();
if (language == "he") {
return "iw";
} else if (language == "yi") {
return "ji";
} else if (language == "id") {
return "in";
} else {
return language;
}
}
The confusing part was that I wasn’t specifying in
but rather id
. So something somewhere was over correcting for me, as we can see in the function above, id
is converted to in
.
So…could I rename the resource strings_id.properties
to strings_in.properties
and call it a day? Let’s try it!
❯ exa -T src/main/resources
src/main/resources
├── strings.properties
├── strings_es.properties
└── strings_in.properties
// Indonesian Bundle
bundle = ResourceBundle.getBundle("strings", Locale("id"))
greeting = bundle.getString("greeting")
println("greeting = $greeting")
//=> greeting = halo
AHA!
Solution
So while that works, it’s misleading and may cause errors if…when I forget why I did that in the first place, so my final solution is to use the 3-letter code as specified in the ISO-639-2 Standard. Finally landing on this:
❯ exa -T src/main/resources
src/main/resources
├── strings.properties
├── strings_es.properties
└── strings_ind.properties
// Indonesian Bundle
bundle = ResourceBundle.getBundle("strings", Locale("ind"))
greeting = bundle.getString("greeting")
println("greeting = $greeting")
//=> greeting = halo