How to improve your properties game in your i18 Spring application with more meaningful value placeholders?

Viktor Reinok
2 min readApr 7, 2024

--

One problem with regular out-of-the-box properties is that it is based on regular numeric indexes {0} and due to edge case or longer properties, the order of indexes could get mixed up and cause an embossing incident. This article will show you how to avoid it.

An example:

transation.1.en=I want to {0} my {1}
transation.1.de=Ich will mein {1} {0}

The problem in the example above, we see that the order of the sentence is swapped. The metal load to map the correct variable is heavy. Let's try to reduce it!

_________

What would be better and more understandable is the following:

transation.1.en=I want to {action} my {program_name}
transation.1.de=Ich will mein {program_name} {action}

First, we need to find a way how to add string rather than number-based place valueholders in properties. In Spring, one could achieve that with a special postfix: #{‘$’}

transation.1.en=I want to #{'$'}{action} my #{'$'}{program_name}
transation.1.de=Ich will mein #{'$'}{program_name} #{'$'}{action}

Now, let us write a service that will replace those values:

@Service
public class PropertyServiceImpl implements PropertyService {

private final Properties properties;

public PropertyServiceImpl() throws IOException {
this.properties = new Properties();
this.properties.load(getClass().getClassLoader().getResourceAsStream("i18.properties"));
}

@Override
public String getProperty(String propertyKey, String language, AbstractMap.SimpleEntry<String, String>... keyValuePairs) {
String propertyValue = properties.getProperty(propertyKey + "." + language);
for (AbstractMap.SimpleEntry<String, String> keyValuePair : keyValuePairs) {
propertyValue = propertyValue.replace("#{'$'}", "");
propertyValue = propertyValue.replace("{" + keyValuePair.getKey() + "}", keyValuePair.getValue());
}
return propertyValue;
}

}

Let's try to test whether it works:

@RestController
public class ExampleController {
private final PropertyService propertyService;

public ExampleController(PropertyService propertyService) {
this.propertyService = propertyService;
}

@GetMapping("/translation")
public String getTranslation(@RequestHeader("x-language") String language) {
return propertyService.getProperty("transation.1", language, getKeyValuePairsFromDb(language));
}

/**
* Get the key value pairs from the database Mock.
*
* @param language en or de
* @return the key value pairs
*/
AbstractMap.SimpleEntry<String, String>[] getKeyValuePairsFromDb(String language) {
AbstractMap.SimpleEntry<String, String>[] keyValuePairs;
if ("en".equals(language)) {
keyValuePairs = new AbstractMap.SimpleEntry[]{
new AbstractMap.SimpleEntry("action", "test"),
new AbstractMap.SimpleEntry("program_name", "car")
};
} else if ("de".equals(language)) {
keyValuePairs = new AbstractMap.SimpleEntry[]{
new AbstractMap.SimpleEntry("action", "testen"),
new AbstractMap.SimpleEntry("program_name", "Auto")
};
} else {
throw new IllegalArgumentException("Unsupported language: " + language);
}
return keyValuePairs;
}

}

Let's add an integration test to prove whether it works.

@WebMvcTest({ExampleController.class, PropertyService.class})
class ExampleControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Test
void getExampleProperty_en_returnsEnPropertyWithValues() throws Exception {

mockMvc.perform(get("/translation")
.header("x-language", "en")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string("I want to test my car"));
}

@Test
void getExampleProperty_de_returnsEnPropertyWithValues() throws Exception {

mockMvc.perform(get("/translation")
.header("x-language", "de")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string("Ich will mein Auto testen"));
}

}

The example is available on GitHub. Check the refs below!

References

  1. The GitHub repo https://github.com/vikreinok/srping-propeties
  2. https://stackoverflow.com/questions/48827440/skip-spring-placeholder-substitution/48827780#48827780

--

--