What are the best practices in Vaadin to internationalize the UI? The current approach of holding a locale in
com.vaadin.Application is not flexible enough as:
Application is only available in
com.vaadin.ui.Component when this component is actually attached to UI. But often one needs to initialize component UI elements before that happens (e.g. define table columns in table constructor or before the table is added to layout).
Application is not assessable in non-UI elements which may wish to localized messages, for example containers that access DB (or other services) lazily and want to fetch locale-specific information; or field validators.
I would expect a static entry-point to access locale-specific messages and current locale, for example:
public static final Locale getSessionLocale() { ... };
public static final String getMessage(String messageId) { ... };
The above methods should manipulate with variables associated with current session (e.g.
{ sessionIdD → [ locale, resourceBunle ]
} )
Maybe above should be a part of
com.vaadin.Application ? I have no good solution, as Session object is not available in application, so above needs support on different Vaadin levels.
Kim , thank you a lot for your input and information.
I just had a quick view into i18n project: the code which is now in
org.vaadin.appfoundation.i18n.Lang is nice to have in Vaadin core in some incarnation.
I18nForm is nice helper as well, we miss it in core.
Unfortunately, I cannot use the project as is, because all my translations are in standard Java property files, and that project uses XML translations file in it’s own format, which is not a big stopper, but adds some complexity to migrate…
Maybe you can also satisfy my curiosity and tell me, what is the role of
InternationalizationServlet ? It extends
HttpServlet , but does not seem to override any methods, so I would say, it could be a standalone helper…
And at what point one need to call
Lang.initialize(Application) ? If you call it from
Application.init() , I think we have a memory leak, as more and more listeners (new
Lang objects) are added to shared application context instance. As
Lang refers
Application , it may really become an issue. The thread local of course will keep the reference to latest instance… Ideally
Lang should be instantiated together (and one time) with
ApplicationServlet .
Thank you for the feedback, only with it am I able to improve the add-on.
TMX is pretty much an industry standard on defining translation messages, however, you are absolutely correct and this is something that will be fixed in the next release. I’ve already made
a fix for this in the development repository . Basically, instead of giving a TMX file, you implement a TranslationSource interface which is responsible for fetching the translation messages and providing them to the servlet. This means that you can make an implementation of the interface which reads your java property files and serves them to the servlet.
Once again, I agree with you. It was a design error and it could as well be a static class of its own. The important part is that the translation files are only read once, for example, when the application is deployed. The translation files should not be re-parsed for each application instance.
It should be called in the application’s init() method. I don’t think there is a memory leak, as the Lang class should be “eaten” by the garbage collector at the same point as the application instance is removed (there are no static references, except for the thread local which is cleaned at the end of the HTTP request). Secondly, the lang object is application specific and cannot be static, as it contains details about which the locale the
current user has chosen. In other words, one user could be browsing the application in English while another could be simultaneously browsing it in Finnish.
If the Lang class would be completely static, then we would need to have a way to get the user’s locale from some other source. An option would be that the application class itself would implement the ThreadLocal pattern and provide the current user’s locale in a static way, but I’m not very fond of this approach as it brings a bit more complexity to the implementation.
After having a closer look at Vaadin sources I see that lifespan of
ApplicationContext is HTTP session. Then indeed there is no memory leak. The context might have several applications, but I can’t imagine how it can happen.
Absolutely agree with you. What I really meant (sorry for not being very clear) is the static access to locatization helpers. Finally I came up with the code like below (which works for me and I am happy), but of course it is not perfect. Ideally, it should be wired with the context directly somehow.I think, I18N should be in the core
public class TranslationUtils implements TransactionListener {
private static ThreadLocal<TranslationUtils> instance = new ThreadLocal<TranslationUtils>();
private final Locale locale;
private final ResourceBundle resourceBundle;
private TranslationUtils(Application application) {
this.locale = application.getLocale();
this.resourceBundle = ResourceBundle.getBundle(TranslationUtils.class.getPackage().getName() + ".messages",
locale);
}
public static final Locale getSessionLocale() {
return instance.get().locale;
}
public static final String getMessage(String messageId) {
try {
return instance.get().resourceBundle.getString(messageId);
}
catch (MissingResourceException e) {
return messageId;
}
}
public static void init(Application application) {
// Connect to application context but only once:
if (instance.get() == null) {
final TranslationUtils translationUtils = new TranslationUtils(application);
application.getContext().addTransactionListener(translationUtils);
instance.set(translationUtils);
}
}
/**
* @see com.vaadin.service.ApplicationContext.TransactionListener#transactionStart(com.vaadin.Application,
* java.lang.Object)
*/
public void transactionStart(Application application, Object transactionData) {
if (instance.get() == null || instance.get().locale != application.getLocale()) {
if (instance.get() != null) {
application.getContext().removeTransactionListener(instance.get());
}
instance.set(new TranslationUtils(application));
}
}
/**
* @see com.vaadin.service.ApplicationContext.TransactionListener#transactionEnd(com.vaadin.Application,
* java.lang.Object)
*/
public void transactionEnd(Application application, Object transactionData) {
}
}