Using standard OO techniques instead of Property Files for I18N and L10N

Using property files / message bundles for I18N / L10N and configuration purposes is problematic. Here’s why:

  • Property files are not refactoring safe.
  • They are in fact interfaces but rarely treated a such.
  • They are often a dump for everything that needed to be configurable a day before the iteration ends.
  • They’re often not well enough documented or not documented at all because documenting property files is a hassle (no standards, no javadoc)
  • …or they consist entirely of comments and commented out statements.
  • They make testing harder than necessary.
  • Often properties are created ad-hoc.
  • Validation logic and default handling inside the application is error-prone.
  • Missing definitions lead to runtime errors, undetected errors or awkward messages like “Error: No {0} in {1} found”.
  • Sometimes the file is not loaded because someone cleaned up src/main/resources.
  • Most property files are loaded on startup or only once.

How can we get rid of property files? I’d like to show you a straight forward solution in Java that will work well if you are not afraid of recompilation. Let me give you an example. This is what is common practice in Java:

# Find dialog
dialog.find.title=Suchen
dialog.find.findlabel.text=Suchen nach
dialog.find.findbutton.text=A very long text that needs to be wrapped but the developer does not know that it's possible - this really happens!
...

Most applications I’ve seen have endless definitions of properties for the UI. I swear I have never ever seen a non-developer change these property files!

The alternative is so simple I’m almost afraid to show it, but it has been extremely useful.

Define a name for each value and add it as a simple read-only property to an interface. Provide concrete implementations for each required language / locale.

 

package com.acme.myapp.presentation.find;

public interface FindDialogResources {
     public String getDialogTitle();
     public String getFindLabelText();
     public String getFindButtonText();
     public String getFindNextButtonText();
     public String getCancelButtonText();
     public String getIgnoreCaseText();
}

// Implementation in myapp-ui-resources-de_DE.jar

package com.acme.myapp.resources;

public class FindDialogResoucesBundle implements FindDialogResources {
    public String getDialogTitle() { return "Suchen"; }
    public String getFindLabelText() { return "Suchen nach";  }
    ....
}

// alternative to enable dynamic change of language:

public class FindDialogResources_de_DE implements ...

public class FindDialogResources_en_US implements ...

Resources that are subject to L18N and I10N must be externalized. But the way of externalization should conform to good software engineering practice.

I like the approach of linking a different resource JAR to smart client applications but you can also load classes by package- or name prefix to enable dynamic switching of languages (for web apps).

Advantages over property files:

  • Clean, minimal, intention-revealing interface (obviously).
  • Refactoring safe.
  • Statically typed and automatically linked by the JVM.
  • Missing definitions are compile-time errors, missing values are easily detected using reflection-based tests.
  • No assumptions about the final implementation.
  • Self-documenting
  • Interface defined in the same module.
  • Implementations can be delivered in other modules or (OSGi) fragments / language packs.

The builder that creates a UI entity requires a concrete implementation of this specific interface as a dependency. This principle can be applied to web applications as well, for example my exposing the resources as a managed bean. Declaring values in Java-Beans style comes in handy for auto-completion in Facelet templates.

public class SwingFindDialogFactory {
    private final FindDialogResources resources;

    public SwingFindDialogFactory (FindDialogResourceBundle resources) {
        requireNonNull(resources); // precondition
        this.resources = resources;
    }

    public FindDialog createInstance(...) {

        ...
        final JLabel findLabel = new JLabel(resources.getFindLabelText());
        final FindButtonAction findAction = new FindButtonAction(resources.getFindButtonText(), resources.getFindNextButtonText());
        ...
    }
}

...

// web app with JSF 2:

@Model
public class Resources implements FindDialogResources {
 private FindDialogResources delegate;

 @PostConstruct
 public initResources() {
     resources = application.getLocalizedFindDialogResourcesForPrincpipal();
 }

 public String getDialogTitle() {
     return delegate.getDialogTitle();
 }

// usage in facelet definition

<h:form>
 ...
 <h:outputText value="{resources.nameLabelText"} />
 <h:inputText value="{myUseCaseForm.name} />
 ...
</h:form>

As a bonus, you can:

  • Fallback to a default language (i.e. EN_us) if translation is not yet complete using a decorator.
  • Automatically validate each ResourceBundle implementation in the CI pipeline for completeness. For example by invoking all methods reflectively and checking results for non-empty values (a pretty good indicator if someone got a call during translation…).
  • You can easily generate and update the manual using simple programs written in Java as I’ve shown in other posts.
  • You can even wrap property files or other data sources if this is required by company policy.
Advertisements

Things you probably never tried with Java

Use a specific language for specific problems they say. Who’s they? A well-engineered general purpose language is often all you need for both small and large projects.

There are some good reasons for focussing on one language/environment:

  • You will get better and more creative in your main language.
  • Reuse the same tools for the final product and your infrastructure.
  • There’s not one specialist for “the ugly legacy build system we cannot get rid off”.
  • Apply the same rules on  code quality and design to scripts, build programs and other tools.

Scripting

Ever waded through historically grown, procedural Unix shell scripts? Let’s face it: BASH does not scale well. Write your scripts in Java. An example for a start script of a fictional application server.

#!/bin/jscript -Xmx1g

import com.acme.appserver.*;
public class StartAppServerWithTestSetup extends GenericAppServerStartup {

    public static final Port HTTP_PORT_TEST_ENVIRONMENT = new Port(4242);
    [...]

    public static void main(String[] args) {
         final AppServerConfigurationBuilder builder = new AppServerConfigurationBuilder(args);
         builder.changeHotDeploymentPath(TEST_PATH.resolve("integrationtest-deployments"));
         builder.changeHttpPort(HTTP_PORT_TEST_ENVIRONMENT);
         builder.changeDefaultDataSource(DATA_SOURCE_TEST_ENVIRONMENT);
         builder.changeDefaultLoggingDirectory(TEST_PATH.resolve("test-logs"));
         final AppServerConfiguration configuration = builder.build();

         final AppServer appServer = new AppServer(configuration);
         appServer.run();
    }
}

What are the basic requirements for the jscript application?

  • Extract Java code to identifiable directory.
  • Set up classpath for compiler (dependency:build-classpath might be of help).
  • Compile code to a designated repository using javac.
  • Prepare VM with classpath including script class and execute.

The only thing needed is a script or C application to prepare the code and bootstrap the JVM.

Build Process

Building Java systems in an enterprise environment is mostly done with Maven or Ant. Ant can do anything but it easily gets out of hand because of duplication. Maven tries to accomplish a lot of things, but who tried to implement a Continuous Delivery has noticed that Maven isn’t quite the fit.

Do you need to go down the so-called Groovy-way with Gradle? Give Java a try. Simple JAR modules can be build with a generic process, integration test steps may profit from the expressiveness of Java as an OO language. The Java Compiler API is a bit tricky but anybody should be able to compile Java files within an hour. Writing a JUnit adapter is a very good exercise to learn and think about class loading.

public class MyComponentTestBuildStep implements BuildStep {
   private final Pipeline pipeline;

   ...

   private final Path testSources;

   private BuildResult buildResult;

   @Override
   public void executeBuildStep() {
        final CompilerPlugin compilerPlugin = new CompilerPlugin(this, pipeline.getClassPathForTestScope());
        compilerPlugin.compiler(testSources);
        if(!compilerPlugin.wasSuccessful()) {
            markAsNotSuccessful(compilerPlugin);
            return;
        }

        final JUnitPlugin testRunnerPlugin = new JUnitPlugin(this,pipeline.getClassPathForTestScope());
        testRunnerPlugin.runTests(compiler.getPathOfCompiledClasses());

        final SurefireTypeReportGeneratorPlugin testReportPlugIn = new SurefireTypeReportGeneratorPlugin(this);
        testReportPlugIn.publishTestReports(testRunnerPlugin);

        if(testRunnerPlugin.wasSuccessful()) {
            markAsNotSuccessful(testRunnerPlugin);
            return;
        }

        markAsSuccess();

   }

   public void markAsNotSuccessful(final BuildPlugin buildPlugin) {
        Preconditions.require(!buildPlugin.wasSuccessful());
        buildResult = buildPlugin.getBuildResult();

   public void markAsSuccessful() {
       buildResult = null;
   }

   public boolean wasSuccessful() {
     return buildResult == null;
   }

   public BuildResult getBuildResult() {
      Preconditions.require(!wasSuccessful());
      return buildResult;
   }
}

This is just a little example to give you an impression how a build program may look like. If you don’t do continuous delivery you might not use the pipeline metaphor to structure your code.

It gets interesting when your Maven build(s) includes lots of specific plug-ins that are not well-maintained or hard to extend. A regular example is the generation of JAXB sources where there are several plug-ins that show awkward behaviour.

You can apply the same quality standards and testing principles for your build process as you do with production code. I have seen projects where several tests where not run on the build server because the tests did not end with Test.

Leverage the power of Java type checking and IDE code completion

XML or JSON are fine for some applications, but for some kinds of applications encoding data in Java classes is much more elegant, especially when you have a more flexible and dynamic way of structuring your modules than Maven and automatic updating. Here’s an example of how to encode Maven dependencies for a Java based build system:

public class ABuildProcess {
   ...

   // <dependencies>
   //   <dependency> ...
   public void setUpDependencies() {
      DependenciesBuilder builder = new DependenciesBuilder();
      builder.addDependency(org.apache.hadoop.hadoop_client.JAR.v1_1_2);
      builder.addDependency(org.apache.hadoop.hadoop_examples.JAR.v1_1_2);
      changeDependencies(builder.getDependencies());
   }
}

I wrote a little tool that provides a Maven repository in parts or as a whole as code-completion friendly Java classes. In most projects not all dependencies change at the same rate. Third-party dependencies are usually static throughout longer periods. You can use your IDEs global search to find all references to hadoop_client.JAR.v1_1_2 and either change the manually or streamline the usage by refactoring away unwanted duplication. A lot of tasks become much simpler when using a Java domain model compared to internal Maven representation of its POM.

Hints

A build process is a program and project of its own. You will probably notice that at first, that the degree of freedom is at least irritating or even hurtful in big projects. But once set up and well-maintained, fine tuning as well as significant changes and refactorings of the build process are much simpler than with declarative tools such as Maven or Ant. Eclipse JDT is all you need. Customizing a Maven plug-in calls for duplicated information and changes that ripple through the build.