Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migration of unmigrated content due to installation of a new plugin

...

  • The dialogs prefix is not relevant and noisy. It was historically introduced to separate those labels from the page templates and page components names and description. Indeed, we're likely to have a stkFAQHeader.name somewhere. Currently leaning towards using separate message bundles. Or have an non-mandatory prefix/suffix (i.e there's a chance the component's title/name needs the same label as its first tab ?)
  • The pages.faq statement is arbitrary and is derived from the fact that the dialog in question happens to be configured under an arbitrary folder structure ( pages/faq/ )

Message bundles

i18nBasename is the property we use to look up a message bundle. This tells the system where the translation files are. It is called "basename" because i18n mechanisms typically append the locale to this and use the result as a path to a file (eg lookup <mybasename>_de_CH.properties, then <mybasename>_de.properties, then <mybasename>.properties - some even go as far as going up the path hierarchy (parent folders) of the basename until they find what they're looking for)

Up to Magnolia 4.5, the i18nBasename property was defined in a dialog definition (or further down). With 5.0, this exists in DialogDefinition, but also in one level below (in FormDefinition), and still exists in all elements below (TabDefinitionFieldDefinition, ...). The property is also present in ActionDefinition (members of DialogDefinition).

In 99% of the cases, the i18nBasename property set at dialog level should be enough. It is useful to keep the possibility to redefine it in actions, forms, tabs, and fields, but it should not be necessary. Defining i18nBasename at module level would be ideal - in terms of minimalizing redundancy anyway - but I'm not sure we'd have support for that right now. It'd be interesting to have i18nBasename in a module descriptor though. It would still be possible for individual components to override it if needed. We could also create a naming convention for i18nBasename as well (see below).

However, with the proposed key naming scheme (as well as with the existing informal one), message keys are distinct enough to consider dropping the need for specifying a message bundle. Proposal:
  • We don't need to specifiy i18nBasename anymore for translatable items. (but we can, at the very least to maintain backwards compatibility)
  • Every module will still have their own message bundle file(s); the system will chain and look for messages in all of these
    • We could imagine having a check that would warn, or even fail, when several bundles contains the same key(s).
    • However we still need to be able to override messages (for projects).
  • Global chains of message bundles - look into all known bundles
  • Basename helps grouping translation work - "I am now translating module X" - but that doesn't mean the basename has to be specified necessarily
  • Order of message bundles chain would need to be consistent and predictable

 

Message-bundles-naming-convention

Instead of having a huge fat bundle within ui-admincecentraladmincentral, every app should have its own bundle. The naming of an app-specific bundle should have the following pattern:

app-<app-name>-messages_<locale>.properties (e.g. app-security-messages_en.properties, app-contacts-messages_en.properties, etc. ).

It should be located directly under <module>/src/main/resources/mgnl-i18n/ (e.g. security-app/src/main/resources/mgnl-i18n/app-security-messages_en.properties)

 

 

Date formats and other localized items

We should make sure things like ColumnFormatter not only use the current user's locale, but also that this is indeed an "enabled" locale. A UI entirely in english but with a date formatted in french would be silly.

Implementation

Introducing a couple of concepts. The basic API will be in its own module in magnolia_main; it doesn't need to be in core, and will maybe not even depend on it. It could even be outside of magnolia_main if there is no dependency to core, but we currently lack a good location for such modules (wink)

Module and package

magnolia-i18n in magnolia_main would contain the API and some implementation. Key generators etc would live near their counterparts (i.e in _ui mostly)

I would simply use info.magnolia.i18n as a package name. However, if this ends up only being usable within the context of magnolia_ui, I would move it there instead of _main, and reflect that in the module and package names.

API

  • @I18nable annotation. "Internationalizable": marks any object as a candidate for translations. Used on interfaces/classes such as FieldDefinition. Is inherited.
  • @I18nText annotation. Marks a String as to be translated.
  • I18nKeyGenerator<T> interface. Implementations generate translation keys for <T>.

Implementation "details"

Exceptions

Exceptions are exceptions. They should not be translated. The message of an exception is targeted at developers and admins, and it is expected that they understand english. (if only to understand the meaning of the exception class name!)

If/when exceptions are currently reported to the user, we need to actually treat the exception. The below shows how not to do this.

Code Block
languagejava
titleIncorrect example
try {
   ... something that fails ...
} catch (SomeException e) {
   uiComponent.alert(e.getMessage());
} 

The below is what we should be aiming for (complete solution pending)

Code Block
languagejava
try {
   ... something that fails ...
} catch (SomeException e) {
   // if there is added value: log.error("sumfin happened: {}", e.getMessage(), e);
   // don't feel forced to bloat the logs, though.
 
   // if this is something the *user* should be informed about, and *can* do something about:
   uiComponent.alert(i18n.getMessage(e));
 
   // but really, is it ? Can a user do anything about a JCR Exception ?
   throw new RuntimeException(e); // did you need to catch it in the first place ?
} 

Of course, the question is - what IS i18n.getMessage(e) ?

This is TBD. An idea is to have an extra method to decorate exceptions, like we suggest for non-configured texts in 

Jira
serverMagnolia - Issue tracker
keyMAGNOLIA-5296
. A pattern we've used in the past (see login form) is to use the exception class simple name (IOException as opposed to java.io.IOException). I don't think we should try and use the message of the exception, because there is absolutely nothing that prevents anyone from injecting arbitrary text in there. However, some exceptions do have some sort of convention - a PathNotFoundException's message is usage the path that was indeed not found. If possible, I'd favor using explicit properties of the exception rather than the message (getPath() if it existed, in this case).

But: this would spread the use of I18nizer much further than it should (remember that one of the initial goals was to completely hide from the developers and have the decoration done by node2bean or Guice). Perhaps it could be "hidden" in uiComponent.handleException(e), and/or in a specific service. Need to think about the context, too (which would be lost if we use this last suggestion)

Implementation

Introducing a couple of concepts. The basic API will be in its own module in magnolia_main; it doesn't need to be in core, and will maybe not even depend on it. It could even be outside of magnolia_main if there is no dependency to core, but we currently lack a good location for such modules (wink)

Module and package

magnolia-i18n in magnolia_main contains the API and the underlying. Key generators live near their counterparts. (i.e in _ui mostly)

The main package is info.magnolia.i18nsystem as a package name.

API

  • @I18nable annotation. "Internationalizable": marks any object as a candidate for translations. Used on interfaces/classes such as FieldDefinition. Is inherited.
  • @I18nText annotation. Marks a String as to be translated.
  • I18nKeyGenerator<T> interface. Implementations generate translation keys for <T>.
  • TranslationService
  • I18nizer. Transforms/decorate a given object and internationalizes it.
  • I18nParentable. This is an interface that allows setting a "parent" on an object. This interface should not be used by client code.

Implementation "details"

  • Tried to implement a Guice module that would do this transparently; also tried to implement this at Node2Bean level. Neither worked - didn't find a way to "swap" instances in Guice (i.e replace bean created by n2b by a proxy)
  • I18nizer thus needs to be invoked explicitely. We have one implementation based on ProxyToys which creates a proxy. It intercepts methods that return other @I18nable objects (by returning one instance, a collection, or a map where value are of an annotated type) and decorate those. Those proxies in turn intercept
  • A Guice module which intercepts the creation of objects annotated with @I18nable and proxies them
  • Said proxy intercepts method calls annotated with @I18nText and returns translated values by delegating to i18nKeyGenerator and TranslatorService.
  • I18nizer also "injects" the I18nParentable interface and sets the parent object while intercepting calls.

Update tools and tasks

To migrate our own modules, we can write a tool which:

  • Starts up a repo and a specific (or several) component managers
  • Load up a translation file we want to migrate (or all its language counter parts)
  • Imports a bootstrap file we want to migrate
  • This should instantiate a whole bunch of forms etc
  • Go through these one by one
    • Go through each @I18nText property of the object
      property of the object
      • Does it correspond to a key currently existing in the translation file ?
        • yes: replace key in translation file by deduced key, remove property from jcr
        • no: add key in translation file
      • If it's a key but it doesn't have an existing translation
        • add deduced key to translation file (instead of configured key), remove property from jcr
      • If it's not a key, i.e current configuration has an "hardcoded" text
        • warn, blow up, panic, ...?
        • add deduced key to translation file (instead of configured key)
        Does it correspond to a key currently existing in the translation file ?
        • yes: replace key in translation file by deduced key, remove property from jcr
        • no: add key in translation file
      • If it's a key but it doesn't have an existing translation
        • add deduced key to translation file (instead of configured key), remove property from jcr
      • If it's not a key, i.e current configuration has an "hardcoded" text
        • warn, blow up, panic, ...?
        • add deduced key to translation file (instead of configured key), remove property from jcr
    • Track unused keys in translation files
    • Track possible duplicates
  • Re-export file to replace bootstrap file
  • Re-export translation file(s)

The same tool could maybe be used to generate update tasks, or generate some sort of config/mapping file passed to a specific update task: it will only need a list of properties to be removed from nodes, with their original value so that we don't remove a property that's been modified by a user.

Message bundles for non-english texts

Bundle languages in separate jars

    • Track unused keys in translation files
    • Track possible duplicates
  • Re-export file to replace bootstrap file
  • Re-export translation file(s)

The same tool could maybe be used to generate update tasks, or generate some sort of config/mapping file passed to a specific update task: it will only need a list of properties to be removed from nodes, with their original value so that we don't remove a property that's been modified by a user.

Message bundles for non-english texts

(lightbulb)Bundle languages in separate jars

  • english/master language still bundled with each module, other languages bundled in 1 jar (1 jar per language, containing translations for many modules)
    • on git, this would be, for example languages/french.git - which would contain N files.
    • These language bundles could be a module. They could thus be versioned, and have dependencies. Dependencies to module they translate would be marked as optional, but we would be able to maintain some sort of version-compatilbility between modules and translations - granted, that wouldn't be super simple to manage - where, for example, we'd update a dependency when keys have been added/removed from that dependency.
    english/master language still bundled with each module, other languages bundled in 1 jar (1 jar per language, containing translations for many modules)
  • maintenance is somewhat easier
  • but at the same time we might get "dependency" issues when modules add/remove keys
  • if we have tools for migration/validation of existing translations, the same tools could be used, perhaps as a maven plugin or sthg.
    • such a tool could potentially help enforcing compatibility between versions (i.e keep a key that was removed in version X+1 of module M)
  • Need some sort of version handling - keep keys for older versions, add keys for newer ones ...
    • Chain (overlay) current translation file with older ones ?

...

  • Enable inline translations within a dialog
    • (minus)I'm not sure the current proposal would work to enable inline translations. It'd be nice for translators to have at least a hint of what the key used for a specific item is. And if we somehow have elements explicitly use the KeyGenerator and other API methods for this, we might as well get rid of the proxy magic and use consistently... 
  • Review process for in-house maintained translations as well as for contributed ones (currently relying on Google spreadsheet)
    • Have a Magnolia-hosted (or SaaS) tool to replace the google spreadsheet
      • some rules like "a translation needs to be validated by 2 other persons to be applied", "once applied it can't be changed directly - only via a 'request'", ...
      • could have a "MessagesManager" impl that fetches translations from this service
      • candidates so far: Transifex (am in contact with CEO Dimitris Glezos, who seems eager to help), CrowdinWebTranslateIt, ... probably others.

Roadmap

  • 5.1 : system in place, translations migrated for AdminCentral and most modules (DAM, STK, ...)
    • Module updates can be postponed
  • ? : extract languages other than english into language-based bundles - language-based bundles - needs a separate concept.
  • ? : review processes for translating Magnolia, both internally and externally. Get contributions. With language files extracted from their modules, it might be easier. - needs a separate concept.

Status

...

Main issues:

  • Jira
    serverMagnolia - Issue tracker
    keyMAGNOLIA-5268
  • Jira
    serverMagnolia - Issue tracker
    keyMGNLUI-1826

(tick) Validate Concepts

(tick) Validate Roadmap

(tick) API

(tick) Finalize module name, location, and package name.

(tick) Implementation details (proxy, Guice module, ...)

(tick) Parental relationships

(error) How does this work with Multi/composite fields

(error) Check things like info.magnolia.ui.form.field.definition.DateFieldDefinition#getDateFormat - might actually work with @I18nText or another annotation

(error) Is there a conflict with merged objects ? (i.e template definitions merged with site def prototype - but also look at other merge cases!!)

(error) An i18Basename could also be defined in site definitions (for in-template translations) - does this work ?

(error) Clarify what to be with i18nBasename properties - the current tendency is to get rid of them. Adapt update tools as needed.

(error) Replace MessagesManager with a cleaner impl

(error) Migration and updates

(error) While this implementation has focused on definition objects, (some) "live" objects already know their parent (see for example 

Status

Main issues:

  • Jira
    serverMagnolia - Issue tracker
    keyMAGNOLIA-5268
  • Jira
    serverMagnolia - Issue tracker
    keyMGNLUI-1826

(tick) Validate Concepts

(tick) Validate Roadmap

(tick) API

(error) Finalize module name, location, and package name.

(tick) Implementation details (proxy, Guice module, ...)

(tick) Parental relationships

(error) How does this work with Multi/composite fields

(error) Check things like info.magnolia.ui.form.field.definition.DateFieldDefinition#getDateFormat - might actually work with @I18nText or another annotation

(error) Is there a conflict with merged objects ? (i.e template definitions merged with site def prototype - but also look at other merge cases!!)

(error) An i18Basename could also be defined in site definitions (for in-template translations) - does this work ?

(error) Clarify what to be with i18nBasename properties - the current tendency is to get rid of them. Adapt update tools as needed.

(error) Replace MessagesManager with a cleaner impl

(error) Migration and updates

Topics to validate or research

...

FormItem) - validate and justify the choice to decorate definition objects.

Known bugs or problems:

  • Jira
    serverMagnolia - Issue tracker
    keyMAGNOLIA-5315
  • Jira
    serverMagnolia - Issue tracker
    keyMAGNOLIA-5317
  • we have no proper way, yet to i18n-ize GWT- / Vaadin-Client-Classes; neither info.magnolia.cms.i18n.MessagesUtil nore the i18n-toolz can be applied on those client-classes

 

Topics to validate or research

...

  • A form can be used in different contexts. It should be translatable according to context.
    • TBD: details, examples
    Where do we pass the user context (i.e locale)
    should be translatable according to context.
    • TBD: details, examples
    • Can we set it at injection time ?
    • Do we add an interface for retrieving the locale-to-use ? (Which UiContext could implement for example)

Existing uses and inconsistencies to fix for 5.1

  • (error) info.magnolia.ui.form.AbstractFormItem#getMessages - should not be public
    • Is broken: doesn't use the user locale afaict.
  • (error) info.magnolia.ui.form.AbstractFormItem#getMessage - should not be public
  • (error) AbstractFormItem defines a semi-arbitrary message bundles chain (see AbstractFormItem#UI_BASENAMES)
  • (error) Definition objects( TabDefinition, etc) have an i18nBasename property, which is very redundant with that of the "runtimes" objects ( FormTab, ...). Usage seems consistent ( return definition.getI18nBasename();) but I don't know why this isn't implemented in info.magnolia.ui.form.AbstractFormItem.
  • (error) Definition objects don't have a common interface. If they did, we could move i18nBasename and label in there. OTOH, some of these objects have more than 1 item to translate (label and description, for example).
  • (error) view.addFormSection(tab.getMessage(tabDefinition.getLabel()), tab.getContainer()); ...translates the message from the tab and passes it translated before it's actually "displayed". (while the method argument is called tabName not tabLabel - but that passed object becomes an argument called caption later down the stack) - the below would make this sort of code much more explicit. You pass an object meant to be a label. You translate it explicitly - most likely at the last possible moment. Or we even extend Vaadin component so that they know about I18nItem.
  • (error) getI18nBasename is defined in too many places. It's inconsistent and unintuitive. Why the redundancy between info.magnolia.ui.dialog.Dialog#getI18nBasename and info.magnolia.ui.dialog.definition.DialogDefinition#getI18nBasename for example ?
  • (error) info.magnolia.ui.form.FormBuilder#buildForm still does "StringUtils.isNotBlank(description)", but since this is now decorated, it will never be blank or null. See 
    Jira
    serverMagnolia - Issue tracker
    keyMAGNOLIA-5315
     though.

Suggested key patterns

The below is a rough outline. The 3 goals below are somewhat hard to reach all at once.

...

<dialog-name>.actions.<action-name>

(lightbulb) I introduced the .actions portion here to avoid confusion with fields; consistency would dictate having a .fields or .tabs portion for field and tabs labels too, but that would downplay the conciseness.

(lightbulb) for all dialog-related items, we could also use <dialog-id> and fallback to dialog-name, for further specializing. (warning) Dialog ID is possibly currently not available; if it is, it's a single string concatenating module name and dialog name, which isn't ideal. It'd be sweet to be able to get back to module id (and app id) from a dialog.

Apps:

<app-id>.label
<app-id>.icon # because the icon might have to be localized
<app-id>.description # because a mouse-over title of the app might be interesting ?

<app-id> could be <module-id>.<app-name> or just app-name (same as for dialogs)

App launcher groups:

app-launcher.<app-group-name>.label # we use the app-launcher prefix, as if app-launcher was an app (which we should consider considering, I suppose)

Templates:

<module-name>.<template-name>.title # I think "title" is what we've been using in 4.x - we could use label for consistency, or simply name
<module-name>.<template-name> # same as above
<module-name>.<template-name>.desc # Useful in template selector

(lightbulb) <module-name>.<template-name> is essentially the component ID.

Page components:

<module-name>.<component-name>.title # see remark above
<module-name>.<component-name>
<module-name>.<component-name>.desc

(lightbulb) <module-name>.<component-name> is essentially the component ID.

Workbench columns:

<app-id>.<sub-app-name>.views.<view-name>.<column-name>
Example: configuration.browser.views.list.name for /modules/ui-admincentral/apps/configuration/subApps/browser/workbench/contentViews/list/columns/name/label

Actionbar in subapps:

<app-name>.<sub-app-name>.actions.<action-name>.<name of getter or field annotated withinfo.magnolia.i18n.I18nText>
Example: contacts.browser.actions.uploadaddContact[.label] for /modules/contacts/apps/contacts/subApps/browser/actions/addContact/label

Actionbar in choose dialogs:

<app-name>.chooseDialog.actions.<action-name>.<name of getter or field annotated withinfo.magnolia.i18n.I18nText>
Example: assets.chooseDialog.actions.addContactupload[.label] for /modules/dam/apps/assets/chooseDialog/actions/upload/label


Other elements tbd ?
  • actions in dialog
  • actionbar in subapps
  • workbench/<view>/columns in content apps: column names
  • in workbench/view/columns, we also have formatterClass which should be locale-sensitive
  • app (label in app launcher, tab)
  • page templates
  • page components

...