We are looking into the possibility of loading all messages.properties via a JSON call into a client-side map. Seems like it would generally straightforward to do, though to make it feasible from a performance perspective we’d need to make sure that the request was cached (and perhaps minified). (Or potentially stored via the new storage techniques provided by HTML5–but as we aren’t using HTML5 elsewhere in our app, not sure if we want to introduce it yet).
I know Bahmni is also looking for a solution to this issue so want to see what your latest thoughts were, as well as gather ideas from the communtiy.
Why not just move it entirely client side and then ONLY have properties necessary for the server-side code to work on the server? This to me seems like the logical architecture (at least for me). Loading it via JSON seems REALLY bad – why make an AJAX call when you don’t need to. Change the architecture, don’t add stuff to the existing architecture to make the old, outdated one keep pace with the new.
I don’t know if anyone from the Bahmni team has started looking at the technical approaches for this, but I know it’s prioritized sometime soon…
My guess is that we want a combination of
- A servlet that you can use to fetch all messages, and that correctly responds to HEAD requests so clients can cache it correctly (e.g. not just a gsp page). They’d be potentially updated every time OpenMRS is restarted or any module is started
- Persist these on the client and only update periodically, because there are probably megabytes of messages across OpenMRS and all modules, and I doubt these will compress well enough to fetch them on every page load or for every session
Thinking about this a bit more…
Perhaps if we actually limit to just the refapp-relevant (or
Bahmni-relevant) messages, that will be much less data.
Also, unlike the pattern I set up and you are extending, Bahmni may want to
have messages for all languages downloaded onto the client.
That seems more complicated. How would we determine which are
“relevant”? I could possibly see a request parameter that the servlet
could take in which would limit the message codes to a certain subset,
but I wouldn’t expect that to be the default behavior.
As for downloading all locales, presumably the servlet you propose could
serve up each supported locale into a separate resource on demand,
right? That way you’d have all locales available to the client, but
only incurring the overhead of downloading them to users who actually
needed them for their own use.
Also, I’d think that we would pre-process things such that the “best”
translation for every available code was determined for a given locale
up-front, and served up in full, so that client side code would not need
to navigate the locale hierarchy and find the best match for a given
code, but simply use what it was given. Thoughts?
Was going to weigh in, but Mike beat me to it. Agree that downloading a subset is probably more complicated than we’d like–not having to figure out which ones are “relevant” was part of the reason for implementing this change. If we get it so that the properties are cached (which I think is a must-have regardless) does the size of the download really matter too much?
I was considering both approaches re: downloading just the codes for a specific locale vs all locales. I agree with Mike that if we download on a per-locale basis, we’d want to figure out the “best” translation for a locale at that point. One advantage (I think) of resolving locally would be that we could wait to do that until an actual code is requested.
Ideally, we would have started building the reference application with an
empty message bundle. Instead, we have all the 1.x UI and 2.x UI codes
mixed together, and all the old orphaned codes are still loaded up. (It
bothers me more now that we are taking about downloading megabytes of
messages unnecessarily and probably increasing the browser tab memory
footprint, as compared to when they were just server side.)
We didn’t do this at the time, years ago, and I imagine it would be painful
now to split, and it’s not something you would want to deal with now.
However I would expect that Bahmni will want to actually start from its
clean set of codes.
So I guess I’m figuring that the uicommons approach and the Bahmni approach
will end up being different. (But someone from the Bahmni tech team should
comment on the intended approach, if any research had been done yet.)
It would be unfortunate if we couldn’t find enough common ground between Bahmni and the rest of the OpenMRS community that we end up building two solutions. Why couldn’t we all collaborate on building a single servlet for this, and allow different query parameters to limit what message codes it returns? Then Bahmni could get its slimmed down “Bahmni-only” version, and others could get more expansive version?
We have just started exploring the localization support for Bahmni. It is in a very early stage, but the following are some thoughts.
- As the front end is Angular JS, we are exploring the option of angular-translate which
- Lot of form configurations in Bahmni use the
Concept hierarchies. So, we can leverage the locale based support for Concept API
- Coming to the point of supporting the generic labels and OpenMRS application, the Servlet method proposed by Darius looks interesting.
Transifex is the other thing we are trying to see how we can manage message.properties across multiple languages
We will do some research on our end and will get back with our thoughts.
I had been meaning to look at angular modules that support localization–thanks for pointing out angular-translate, I will l take a look.
It’s looking likely that we will be spiking on a servlet approach (or somethig similar) in the next week or two, so if we do, we will keep you posted.
We’ve had good luck with Transifex–it’s not perfect, and has some annoyances, but it much better than mailing messages.properties files around or manually copying from Excel spreadsheets and/or email into messages.properties files, which was what we did previously.
The nice thing about angular-translate is that it will do asynchronous loading of JSON file with all the messages. Getting a JSON of the messages.properties shouldn’t be difficult. And transifex can off course have JSONs
@mogoodrich What are your experiences with Transifex? Have you used Transifex Live? Tried out yesterday and it is pretty easy to localize the application.
It would be good to have a REST service in OpenMRS which can be pointed to
a folder containing modules of message properties. So for Bahmni it could
The REST service can return JSON which can be consumed by ng-translate
(hopefully, we would spike out whether there are any format related
constraints or just as standard key value as JSOn would work out fine)
We could explore what are the messages which could be potentially used
across RefApp, Bahmni and other implementation. I suspect this list would
be small, actually I cannot think beyond Core patient properties, login
screen labels like first name, middle name (i.e. I am keeping aside
concepts). It would be good to know what else people have in mind for
sharing across ref-app and others like bahmni.
Caching is an important factor here, we should choose a strategy which is
optimised for the content changing very infrequently.
We’ve basically used the original Transifex, not Transifer Live. Pretty straightforward–you push up messages.properties files and there’s a UI that allows translators to submit translations. Transifex can be set up to poll github for changes to a messages.properties source language file and pull in new code/message pairs. There’s no way to automatically pull back messages_*.properties files after translation, but there’s a command-line client that makes the process fairly easy. We’ve got the main OpenMRS modules in Transfex, with documentation here:
Transifex Live looked interesting, but I didn’t look too much into it because at a glance I thought it implied that translations would be pulled in from a Transifex server on-demand when a user requests a page–which would be a no-go for us since most of our implementations run off a local server and don’t require/always have full Internet access. Am I wrong about this? If so, I’d definitely want to explore Transifex Live further.
I think we’ve want a more expansive set of messages.properties than Bahnmi (again, assuming we could cache and avoid performance constraints), but if the REST service and/or servlet could be configured to allow a custom set of messages.properties or just all messages.properties, I think that would work.
I’m looking forward to checking out ng-translate as well.
For what it’s worth, starting spike using angular-translate now… will let you know what I come up with…
I’ve got a very basic controller that serves up message codes to be consumed by angular-translate. The code and ticket can be found here:
An example of how we are using it:
Thanks to @mseaton and @darius for listening to me talking through the design…
Fucntionality is pretty basic–when a request comes in the first time for a specific locale, it builds a key-to-message map for that locale for all keys in the system and returns the json. It persists the map in a static variable, so it doesn’t have to regenerate the map each time.
It also created a random uuid to serve as a “eTag” that it returns with a response. If it receives this eTag a request from a client, it returns a 304 (not modified) instead of the map. So it relies on browser-caching to be enabled and working, or the entire map will need to be downloaded each time, which is large–1.4MB. This is a define limitation/risk (will need to be tested with the thin clients we use at UHM).
@darius do you know if a controller (and static variables associated with it) are refreshed/reset in a context refresh? I suspect not, so at this point if you install a new module you’ll need to restart the server for the codes in the module to be picked up. We could fix this by setting the messages cache and the etag to null on each context-refresh, though we couldn’t do this directly through the context-refreshed method of the activator since the controller is in the web layer and not visible to the activator. (Though as Mike suggested to me it should be easy to add a “needsRefresh” static to the activator which is set to true during a refresh, and which the controller checks each time it is called.)
If Bahmni only wants/needs some of the message codes, seems like it would be straightforward to add some sort of configuration parameter telling the controller to only package up codes with certain prefixes?
Spring controllers are beans, so a new one is instantiated on a context refresh. I don’t know if the classes are reloaded though, so it’s possible that static variables have their values preserved.
I would recommend that you generate the etag as a hash of all the message content. (Because as you have it now, a server restart (and maybe a module refresh) will require clients to redownload the whole thing. It would be preferable to only have to download 1.5MB if something changes.
Good call on the hashcodes, I will implement that.
Will have to test/think about the context refreshed issue a little more.
I believe Spring controllers are singletons… if so, there’s no real reason to make the variables static, correct? And if they aren’t static, then assumedly they would be reset when the bean is re-instantiated? Is my logic faulty at all?