Search code examples
javaeclipselocalizationinternationalizationosgi

How do I internationalize an OSGi application using Eclipse?


I'm trying to internationalize an OSGi application using the "OSGi way" but I haven't had progress. By OSGi way I mean, using the capabilities the framework provides for it. I have internationalized Java applications before but I'd like to know how to do it as an OSGi application.

I have created this simple demo [GitHub repo] which aims to create a bundle that will log a message once it is activated and another message once it is deactivated.

Project structure:

src
   |- org.example.i18n
                     |- SimpleLoggingComponent   // where the actual strings are
                     |- SimpleLogService
                     |- SimpleLogServiceImpl
META-INF
   |- MANIFEST.MF
OSGI-INF
   |- org.example.i18n.SimpleLoggingComponent
   |- org.example.i18n.SimpleLogServiceImpl
build.properties

SimpleLoggingComponent source

@Component
public class SimpleLoggingComponent {

    private SimpleLogService simpleLogService;

    @Reference
    public void bindLogger(SimpleLogService logService) {
        this.simpleLogService = logService;
    }

    public void unbindLogger(SimpleLogService logService) {
        this.simpleLogService = null;
    }

    @Activate
    public void activate() {
        if (simpleLogService != null) {
            simpleLogService.log("Yee ha, I'm logging!"); // <-- need this message internationalized
        }
    }

    @Deactivate
    public void deactivate() {
        if (simpleLogService != null) {
            simpleLogService.log("Done, I'm finishing logging!"); // <-- need this message internationalized
        }
    }
}

For now, the strings are fixed in the code and I'd like to be able to internationalize these. Let's say, have English and Spanish languages supported.

Later I'm planning to add support for more languages by means of Fragment Bundles, so the solution should be ready to be extensible by this means.


I have read all these but I haven't found anything consistent that helps me.

Also, neither the OSGi Alliance Tutorial Archive nor the OSGi enRoute contains anything about it.

Environment:

PS: I'm sure this is not a complicated task, it's just that I haven't found any useful (to me) documentation about it.


Solution

  • Theoretical knowledge

    Localization 1

    Bundle localization entries share a common base name. To find a potential localization entry, an underscore ('_' \u005F) is added plus a number of suffixes, separated by another underscore, and finally appended with the suffix .properties. The suffixes are defined in java.util.Locale. The order for the suffixes must be:

    • language

    • country

    • variant

    For example, the following files provide manifest translations for English, Dutch (Belgium and the Netherlands) and Swedish.

    OSGI-INF/l10n/bundle_en.properties
    OSGI-INF/l10n/bundle_nl_BE.properties
    OSGI-INF/l10n/bundle_nl_NL.properties
    OSGI-INF/l10n/bundle_sv.properties
    

    Manifest Localization 2

    Localized values are stored in property resources within the bundle. The default base name of the bundle localization property files is OSGI-INF/l10n/bundle. The Bundle-Localization manifest header can be used to override the default base name for the localization files. This location is relative to the root of the bundle and bundle fragments.

    A localization entry contains key/value entries for localized information. All headers in a bundle's manifest can be localized. However, the Framework must always use the non-localized versions of headers that have Framework semantics.

    A localization key can be specified as the value of a bundle's manifest header using the following syntax:

    header-value ::= '%'text
    text ::= < any value which is both a valid manifest headervalue
       and a valid property key name >
    

    For example, consider the following bundle manifest entries:

    Bundle-Name: %acme bundle
    Bundle-Vendor: %acme corporation
    Bundle-Description: %acme description
    Bundle-Activator: com.acme.bundle.Activator
    Acme-Defined-Header: %acme special header
    

    User-defined headers can also be localized. Spaces in the localization keys are explicitly allowed.

    The previous example manifest entries could be localized by the following entries in the manifest localization entry OSGI-INF/l10n/bundle.properties.

    # bundle.properties
    acme\ bundle=The ACME Bundle
    acme\ corporation=The ACME Corporation
    acme\ description=The ACME Bundle provides all of the ACME\ services
    acme\ special\ header=user-defined Acme Data
    

    In Practice

    1. First, let's create the bundle files, the ones which will contain the key/value pairs. In this case one for English Language (bundle.properties) which will be the default one and one for Spanish Language (bundle_es.properties)

    ...
    OSGI-INF
       |- l10n
            |- bundle.properties
            |- bunlde_es.properties
       |- ...
    

    ... which will contain our previously hard-coded string values.

    #bundle.properties
    startMessage = Yeah ha, I'm logging!
    endMessage = Done, I'm finishing logging!
    
    #bundle_es.properties
    startMessage = Si, Estoy registrando logs!
    endMessage = Terminado, He concluido de registrar logs!
    

    2. Now let's create an utility component which will help us in getting the values associated to each key according to the locale.

    src
       ...
       |- org.example.i18n.messages
                                  |- MessageProvider
                                  |- MessagesProviderImpl
       ...
    

    There are two files: the interface and an actual implementation which is the one that contains the logic for getting the key/value pairs.

    public interface MessageProvider {
        String get(String key);
    }
    
    @Component
    public class MessagesProviderImpl implements MessageProvider {
        private BundleLocalization bundleLocalization;
        private LocaleProvider localeProvider;
        private ResourceBundle resourceBundle;
    
        @Reference
        public void bindBundleLocalization(BundleLocalization bundleLocalization) {
            this.bundleLocalization = bundleLocalization;
        }
    
        @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
        public void bindLocaleProvider(LocaleProvider localeProvider) {
            this.localeProvider = localeProvider;
            setResourceBundle()
        }
    
        /*unbind methods omitted*/
    
        @Activate
        public void activate() {
            setResourceBundle();
        }
    
        @Override
        public String get(String key) {
            return resourceBundle.getString(key);
        }
    
        private String getLocale() {
            return localeProvider != null ? localeProvider.getLocale().toString() : Locale.getDefault().toString();
        }
    
        private void setResourceBundle() {
            resourceBundle = bundleLocalization.getLocalization(FrameworkUtil.getBundle(getClass()), getLocale());
        }
    }
    

    3. Use the MessageProvider component in SimpleLoggingComponent.

    @Component
    public class SimpleLoggingComponent {
    
        /*previously code omitted for brevity*/
    
        private MessageProvider messages;
    
        @Reference
        public void bindMessageProvider(MessageProvider messageProvider) {
            messages = messageProvider;
        }
    
        /*unbind methods omitted*/
    
        @Activate
        public void activate() {
            simpleLogService.log(messages.get("startMessage")); // <- use now the key: startMessage
        }
    
        @Deactivate
        public void deactivate() {
            simpleLogService.log(messages.get("endMessage")); // <- use now the key: endMessage
        }
    }
    

    Launching the application with a custom language

    On the Arguments tab use the runtime parameter -nl for this purpose, e.g. -nl en

    Image of run configuration with parameter -nl set to English language: <code>-nl en</code>


    References

    Full source code