spring-projects / spring-shell

Spring based shell
http://projects.spring.io/spring-shell/
Apache License 2.0
723 stars 395 forks source link

Allow localization for internal messages. #668

Open KotlinFactory opened 1 year ago

KotlinFactory commented 1 year ago

So here are some examples on how we archived localization in our internal fork. It was still using the old SpringShell 2.x version.

We started by creating a wrapper that contains all SpringShell messages.

package org.springframework.shell.standard;

 // author Olaf Staehle
@Component
public class SpringShellMessages {

    private MessageSourceAccessor acc;

    public SpringShellMessages(final String lang, final String[] clientLoc) {
        final var messageSource = new ResourceBundleMessageSource();
    }

        /**
     * Retrieve the message for the given code in the configured language.
     *
     * @param code           the code of the message
     * @param defaultMessage the String to return if the lookup fails
     * @return the message
     */
    public String getWithDefault(final String code, final String defaultMessage) // Implementation

            /**
     * Retrieve the message for the given code in the configured language and
     * replace parameters in the message with the given arguments.
     *
     * @param code the code of the message
     * @param args a number of arguments that will be filled in for params within
     *             the message (params look like "{0}", "{1,date}", "{2,time}"
     *             within a message)
     * @return the message
     * @throws NoSuchMessageException if no corresponding message was found
     */
    public String get(final String code, final Object... args) throws NoSuchMessageException // Implementation
}

This is then used in the StandardMethodTargetRegistrar

public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
    @Autowired
    public void setApplicationContext(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Autowired
    public void setMessages(final SpringShellMessages messages) {
        this.messages = messages;
    }
}

And finally to determine the group and the description in the register method in the StandardMethodTargetRegistrar

    final var group = messages.getWithDefault(groupcode, groupcode);
        final var description = messages.getWithDefault(shellMapping.value(), shellMapping.value());

We also replaced the String literals in the SpringShell using the SpringShellMessages class

jvalkeal commented 1 year ago

I'm going to look on what other places we need to handle bundles.

jvalkeal commented 1 year ago

With providing some common class user can use, ParserMessage and TemplateExecutor are one internal examples where i18n is needed.

Getting started with boot is super easy as you just create messages.properties, messages_en.properties, etc. For library code few questions remains:

  1. What's naming as you can't have same file in multiple jars
  2. How user would enable i18n features as we should default to en regardless what locale is in use
  3. How user is extending/modifying what spring-shell provides
db-ost commented 1 year ago

I would suggest defining 2 properties that can be set in the project that uses the spring-shell library, e.g.

  1. spring.shell.language that can define a langauge tag like de, en-US, zh-CN, etc. default is en
  2. spring.shell.client_localization that can define a list of names for message files

When creating the message source all message files are combined. As the first hit wins, the custom files should be added first, so one can overwrite the texts of the spring shell. A constructor for a class could look like this (not tested if addBasenames works if no basenames have been set before and parameters may not be null):

public SpringShellMessages(final String language, final String[] clientLocalization) {
    final var messageSource = new ResourceBundleMessageSource();
    messageSource.setDefaultEncoding("UTF-8");
    if (clientLocalization.length > 0) {
        messageSource.setBasenames(clientLocalization);
    }
    messageSource.addBasenames("localization/message");
    final Locale loc = Locale.forLanguageTag(language);
    accessor = new MessageSourceAccessor(messageSource, loc.getLanguage().isEmpty() ? Locale.ENGLISH : loc);
}

You can now use the accessor to lookup the message codes: accessor.getMessage(code); accessor.getMessage(code, defaultMessage); accessor.getMessage(code, args); // with arguments to fill in for parameters in the message

spring-shell would put message_en.properties, etc in its /src/main/resources/localization. A project using the library can define one or multiple basenames as well (other than message) and put its files in its own resources dir. String literals will be put in the message file with a code that is used to lookup the string. To determine if a set command or parameter description should be treated as a literal or as a lookup code, one can either lookup the string and if it is not found use it as default or define a marker prefix for lookup codes.