PlayFramework & GAE localization editor
Play Framework includes the messages-file way of handling the translation of values. Unfortunately, this won't do for the site I'm creating: it has 7 different languages and editors need to be able to translate on-the-fly.
So I thought I'd share the solution that's now implemented: translations via the BigTable datastore.
Datastore
First of all, there's a model:
@Entity
public class Translation extends EnhancedModel {
@Id(Generator.AUTO_INCREMENT)
public Long id;
public String locale;
public String name;
public String translation;
@Index("qidx")
public List<String> q;
@PreSave
@PreInsert
@PreUpdate
public void updateSearchFields()
{
if (this.translation != null && this.translation.isEmpty())
this.translation = null;
this.q = Utils.splitSearchWords(String.format("%s %s", name, translation));
}
}
I'm using Siena 2.0.7, that's where the annotations are coming from. The "q" field gets indexed and the editors can search on that field when they're looking for a translation. OnSave of a record this field also gets updated.
PlayPlugin
Next up is the code that actually translates the &{'translatable message'} that the views are asking for. This is going to be a PlayPlugin overriding the getMessage function:
public class MessagesDbPlugin extends PlayPlugin {
@Override
public String getMessage(String locale, Object key, Object... args) {
return String.format(getTranslation(locale, key.toString()), args);
}
}
Now we're set up to get the actual translations from the database. Of course we need to also cache them using the gae memcached client, as it's much faster then the actual datastore.
private static String getTranslation(String locale, String key)
{
if (Utils.isNullOrEmpty(key))
return "";
// look up in cache
String value = Cache.get(Translation.generateCacheKey(locale, key), String.class);
if (!Utils.isNullOrEmpty(value))
return value;
// look up in database
Translation t = Translation.all().filter("locale", locale).filter("name", key).get();
if (t != null)
{
cache(t);
return Utils.isNullOrEmpty(t.translation) ? t.name : t.translation;
}
// validate the locale: is it possible?
if (!Play.langs.contains(locale))
{
Logger.error("Invalid language: %s", locale);
return "";
}
// not found: insert into the database and the cache for each language
for (String lang : Play.langs)
{
Translation newT = new Translation();
newT.locale = lang;
newT.name = key;
newT.translation = null;
newT.save();
cache(newT);
}
// now return the key, as the value is empty
That's it, you're set! Each time a translation is requested that doesn't yet exist in the database, it gets inserted. So if you accidentally skip one and don't enter it in the database, is pops up in the cms anyway.
I'm not going to display all the javascript, html and css involved in making the translations editable, but it's fairly easy. Getting the translations you searched for is plain and simple:
Translation.all().filter("locale", locale).filter("q", q.toLowerCase().trim()).fetch();
And don't forget to invalidate the cache entry after you saved a new translation!
That's it! I might make a Play Module out of it some day, if enough people ask me to :)