Sunday, October 09, 2016

Spring Boot Configuration Properties Localization

Spring Boot allows to externalize application configuration by using properties files or YAML files. Spring Profiles provide a way to segregate parts of your application configuration and make it available in certain environments (development, qa, production, etc.).

When building applications for multiple countries, you might have some configuration properties that might differ for a specific locale. One way to localize these configuration properties, would be creating a new profile per locale where you can override these values. This approach only works if your application is running for that specific locale, and not supporting multiple locales at the same time. This approach would also increase the number of profiles to be managed (number of application properties files).

Here, I am going to show another approach where you can define the localizable properties inside your existing configuration properties class. For example, if have the following set of properties for a single country in your application.yml file:

region:
  hostSuffix: .com
  currencyCode: USD
  patternShortDate: MM-dd-YYYY

Then, you could localize these properties by defining them for specific locales, for example en_CA and en_GB:

region:
  hostSuffix: .com
  currencyCode: USD
  patternShortDate: MM-dd-YYYY
  locales:
    en_CA:
      hostSuffix: .ca
      currencyCode: CAD
      patternShortDate: dd-MM-YYYY
    en_GB:
      hostSuffix: .co.uk
      currencyCode: GBD
      patternShortDate: dd-MM-YYYY

The idea is to be able to get the desired property by locale:

// Getting a property for the default locale.
regionProperties.getHostSuffix(); // returns ".com"

// Getting a property for supported locales.
regionProperties.fromLocale(Locale.US).getHostSuffix(); // returns ".com"
regionProperties.fromLocale(Locale.CANADA).getHostSuffix(); // returns ".ca"
regionProperties.fromLocale(Locale.UK).getHostSuffix(); // returns ".co.uk"

// Getting a property for unsupported locales (returns the default)
regionProperties.fromLocale(Locale.ITALY).getHostSuffix(); // returns ".com" (default)


Mapping locale String to Locale object

The first thing is to enable Spring to read the locale string, e.g. "en_CA", and convert it to a java.util.Locale object while loading the application configuration properties. For that, we will need to define a Converter class from String to Locale by implementing the Converter interface and adding the @ConfigurationPropertiesBinding annotation to tell Spring to use it when biding the properties.


@Component
@ConfigurationPropertiesBinding
public class LocaleConverter implements Converter<String, Locale> {

    private static final Map<String, Locale> LOCALE_MAP = new HashMap<>(Locale.getAvailableLocales().length);

    static {
        for (Locale locale : Locale.getAvailableLocales()) {
            LOCALE_MAP.put(locale.toString(), locale);
        }
    }

    @Override
    public Locale convert(String s) {
        return LOCALE_MAP.get(s);
    }
}

Loading the properties in your Configuration Proper

Once added this converter, all you have to do in your configuration properties class is to add the following property with its public getter/setter and Spring loads the application.yml (or application.properties) files.

    protected Map<String, Locale> locales = new HashMap<>();

Lets define a generic abstract class with this property and with the method to find the setting by locale (fromLocale):

public abstract class LocalizableProperties<T extends LocalizableProperties> {
    protected Locale locale;
    protected Map<String, Locale> locales = new HashMap<>();

    @PostConstruct
    private void postConstruct() {
        // Set the locale of each entry of the map.
        for (Map.Entry<String, Locale> entry : locales.entrySet()) {
            entry.getValue().setLocale(entry.getKey());
        }
    }

    public T fromLocale(Locale locale) {
        return locales.containsKey(locale) ? locales.get(locale) : (T) this;
    }

    public Locale getLocale() {
        return locale;
    }

    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    public Map<String, Locale&gt getLocales() {
        return locales;
    }

    public void setLocales(Map<String, Locale&gt locales) {
        this.locales = locales;
    }
}

Creating Localizable Configuration Properties

Now, lets define our configuration properties class, RegionProperties, that simply extends our abstract class and define some application properties to be localized:

@Component
@ConfigurationProperties(prefix = "region")
public class RegionProperties extends LocalizableProperties<RegionProperties> {
    private String hostSuffix;
    private String currencyCode;
    private String patternShortDate;

    public String getHostSuffix() {
        return hostSuffix;
    }

    public void setHostSuffix(String hostSuffix) {
        this.hostSuffix = hostSuffix;
    }

    public String getCurrencyCode() {
        return currencyCode;
    }

    public void setCurrencyCode(String currencyCode) {
        this.currencyCode = currencyCode;
    }

    public String getPatternShortDate() {
        return patternShortDate;
    }

    public void setPatternShortDate(String patternShortDate) {
        this.patternShortDate = patternShortDate;
    }
}

That is, any configuration property class that need to be localized can be declared to extend the LocaleProperties class so that it can be localizable.

You can find a running code in my github repo:
https://github.com/lerocha/spring-boot-localizable-properties

No comments: