Introduction

Ontimize Boot’s remote internationalization service (i18n) allows you to manage the translations of the different languages of the application without having to modify the programs translation files, by storing the translations in the database. The interface implemented by the service is com.ontimize.jee.common.services.i18n.II18nService.

Prerequisites

There are 2 options to follow this tutorial, clone the repository with the initial state and follow the tutorial step by step, or download the final example and see which files are new and which have been updated.

Initial project

/$ git clone https://github.com/ontimize/ontimize-examples
/ontimize-examples$ cd ontimize-examples
/ontimize-examples$ git checkout boot-i18n-initial

Final example

/$ git clone https://github.com/ontimize/ontimize-examples
/ontimize-examples$ cd ontimize-examples
/ontimize-examples$ git checkout boot-i18n

Configuring internationalization service in Ontimize Boot

In this complete tutorial, we are going to create a backend for an application from scratch, including the database with different tables, configuring the server, creating the necessary DAO files and implementing the service.

DB configuration

If the configuration table does not exist in the DB, the database tables necessary for the operation of the service are created, one in which the names of the translation bundles shall be stored, and another in which the translations and the bundle to which they belong shall be stored:

SQL (HSQL) Statement

CREATE TABLE TI18N(ID_I18N INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY,CLASS_NAME VARCHAR(150),I18N_DESCRIPTION VARCHAR(250));
CREATE TABLE TI18N_VALUE(ID_I18N_VALUE INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY,ID_I18N INTEGER NOT NULL,"KEY" VARCHAR(250),ES_ES LONGVARCHAR,EN_US LONGVARCHAR,ES_ES_GL LONGVARCHAR);

In the first table (the one named TI18N) we will add a bundle to link all resources of the TI18N_VALUE table.

SQL (HSQL) Statement

INSERT INTO TI18N (ID_I18N, CLASS_NAME, I18N_DESCRIPTION) VALUES(0, 'i18n.bundle', 'Resource bundle in database');

In the case of the second table (the one named TI18N_VALUE) it will contain a column with the name of each Locale accepted by the application.

SQL (HSQL) Statement

INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'ID','Id. Candidato','Candidate id','Id. Candidato');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'PHOTO','Foto','Photo','Foto');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'NAME','Nombre','Name','Nome');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'SURNAME','Apellido','Surname','Apelido');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'BIRTHDAY','Fecha de Nacimiento','Birthday','Data de nacemento');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'DNI','DNI Candidato','Candidate DNI','DNI Candidato');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'PHONE','Telefono','Phone','Telefono');
INSERT INTO TI18N_VALUE (ID_I18N, KEY, ES_ES, EN_US, GL_ES) VALUES(0,'EMAIL','Correo electronico','E-mail','Correo electronico');

Add Permissions

The com.ontimize.jee.common.services.i18n.II18nService service has several methods that we will have to add to the permissions to be able to execute the methods.

SQL (HSQL) Statement

INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/getAllResourceBundles');
INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/getBundles');
INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/getBundle');
INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/getAvailableBundles');
INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/getAvailableLocales');
INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/updateBundleValues');
INSERT INTO TSERVER_PERMISSION (PERMISSION_NAME) VALUES('com.ontimize.jee.common.services.i18n.II18nService/deleteBundleValues');

Add all permissions to the user role demo.

SQL (HSQL) Statement

INSERT
	INTO
	TROLE_SERVER_PERMISSION tsp (ID_SERVER_PERMISSION, ID_ROLENAME)
SELECT
	ID_SERVER_PERMISSION,
	(
	SELECT
		ID_ROLENAME
	FROM
		TUSER_ROLE
	WHERE
		USER_ = 'demo') AS ID_ROLENAME
FROM
	TSERVER_PERMISSION tp
LEFT JOIN TROLE_SERVER_PERMISSION tsp ON
	tp.ID_SERVER_PERMISSION = tsp.ID_SERVER_PERMISSION
WHERE
	tsp.ID_SERVER_PERMISSION IS NULL

Server configuration

To configure this service, a new configuration fragment shall be added to the application.yml file.

ontimize:
   i18n:
      ref-bundle-repository: OCDatabaseBundleDao
      bundle-key-column: ID_I18N
      bundle-class-name-column: CLASS_NAME
      bundle-description-column: I18N_DESCRIPTION
      ref-bundle-value-repository: OCDatabaseBundleValueDao
      bundle-value-text-key-column: KEY
      bundle-value-key-column: ID_I18N_VALUE
      engine: default

This configuration indicates the keys and values to be stored in the database. The database table is the one corresponding to the bean described in the ref-bundle-repository: OCDatabaseBundleDao attribute.

Creation of DAO files

It is necessary to create the DAOs that reflect the new DB tables we have created.

First we will create the *.xml files.

OCDatabaseBundleDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<JdbcEntitySetup
	xmlns="http://www.ontimize.com/schema/jdbc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.ontimize.com/schema/jdbc http://www.ontimize.com/schema/jdbc/ontimize-jdbc-dao.xsd"
	table="TI18N" datasource="mainDataSource"
	catalog="" schema="${mainschema}"
	sqlhandler="dbSQLStatementHandler">
	<DeleteKeys>
		<Column>ID_I18N</Column>
	</DeleteKeys>
	<UpdateKeys>
		<Column>ID_I18N</Column>
	</UpdateKeys>
	<GeneratedKey>ID_I18N</GeneratedKey>
</JdbcEntitySetup>

OCDatabaseBundleValueDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<JdbcEntitySetup
	xmlns="http://www.ontimize.com/schema/jdbc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.ontimize.com/schema/jdbc http://www.ontimize.com/schema/jdbc/ontimize-jdbc-dao.xsd"
	table="TI18N_VALUE" datasource="mainDataSource"
	catalog="" schema="${mainschema}"
	sqlhandler="dbSQLStatementHandler">
	<DeleteKeys>
		<Column>ID_I18N</Column>
		<Column>KEY</Column>
	</DeleteKeys>
	<UpdateKeys>
		<Column>ID_I18N</Column>
		<Column>KEY</Column>
	</UpdateKeys>
	<GeneratedKey>ID_I18N_VALUE</GeneratedKey>
</JdbcEntitySetup>

In the *.java files we indicate that it is a repository whose name will be OCDatabaseBundleDao and OCDatabaseBundleValueDao respectively, using the @Repository annotation. With the @Lazy annotation, we indicate that the loading is delayed until it is completely necessary, and the @ConfigurationFile annotation allows us to configure this DAO using the XML file and an additional file where some features common to several DAOs can be stored, such as the schema they belong to.

OCDatabaseBundleDao.java

package com.ontimize.projectwiki.model.core.dao;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;

import com.ontimize.jee.server.dao.common.ConfigurationFile;
import com.ontimize.jee.server.dao.jdbc.OntimizeJdbcDaoSupport;

@Repository(value = "OCDatabaseBundleDao")
@Lazy
@ConfigurationFile(configurationFile = "dao/OCDatabaseBundleDao.xml", configurationFilePlaceholder = "dao/placeholders.properties")
public class OCDatabaseBundleDao extends OntimizeJdbcDaoSupport {

  public OCDatabaseBundleDao() {
    super();
  }
}

OCDatabaseBundleValueDao.java

package com.ontimize.projectwiki.model.core.dao;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;

import com.ontimize.jee.server.dao.common.ConfigurationFile;
import com.ontimize.jee.server.dao.jdbc.OntimizeJdbcDaoSupport;

@Repository(value = "OCDatabaseBundleValueDao")
@Lazy
@ConfigurationFile(configurationFile = "dao/OCDatabaseBundleValueDao.xml", configurationFilePlaceholder = "dao/placeholders.properties")
public class OCDatabaseBundleValueDao extends OntimizeJdbcDaoSupport {

  public OCDatabaseBundleValueDao() {
    super();
  }
}

Add I18n Rest Controller

I18nRestController.java

With the tag @RequestMapping we indicate the route through which the rest service will receive the requests. With the tag @RequestBody we receive the data that were sent in the rest request. In this class we create a method that retrieves the translations of a bundle that we have specified in the language that we have indicated.

package com.ontimize.projectwiki.ws.core.rest;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Locale;
import java.util.ResourceBundle;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.ontimize.jee.common.dto.EntityResult;
import com.ontimize.jee.common.dto.EntityResultMapImpl;

import com.ontimize.jee.common.services.i18n.II18nService;
import com.ontimize.jee.server.rest.InsertParameter;

@RestController
@RequestMapping("/i18n")
@ComponentScan(basePackageClasses = { com.ontimize.jee.common.services.i18n.II18nService.class })
public class I18nRestController {

	@Autowired
	@Qualifier("I18nService")
	private II18nService iI18nService;

	@RequestMapping(value = "/bundle", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<EntityResult> getBundle(@RequestBody InsertParameter parameter) {
		Map<Object, Object> data = parameter.getData();
		ResourceBundle resource = iI18nService.getBundle(String.valueOf(data.get("bundle")),
				new Locale(String.valueOf(data.get("lang")), String.valueOf(data.get("country"))));
		return new ResponseEntity<EntityResult>(bundleToEntityResult(resource), HttpStatus.OK);
	}

	private EntityResult bundleToEntityResult(ResourceBundle resource) {
		Map<String, String> values = new HashMap<>();
		Enumeration<String> keys = resource.getKeys();
		EntityResult eR = new EntityResultMapImpl();
		while (keys.hasMoreElements()) {
			String key = keys.nextElement();
			values.put("key", key);
			values.put("value", resource.getString(key));
			eR.addRecord(values);
			values = new HashMap<>();
		}
		return eR;
	}
}

Testing the i18n services

To test the i18n service we will use REST requests. For this, the following query will be executed: localhost:33333/i18n/bundle

Element Meaning
localhost:33333 Indicates the host
/i18n Indicates the service to be queried
/bundle Indicates the request mapping class

The request types can only be POST and the request body has to have the following structure:

Element Meaning
bundle Indicates the class name of the bundle specified in the database
lang Indicates the language of the bundle. It needs to be lowercase.
country Indicates the country of the language. It needs to be uppercase.
Request type Query URL Service method Body request
POST query localhost:33333/i18n/bundle getBundle Example below

Body request:

{
    "data": {
      "bundle": "i18n.bundle",
      "lang": "en",
      "country": "US"
    }
}

The authorization used for these requests is authorization of the type BASIC. In this case, the access must be done with a user and password example:

    User: demo
Password: demouser

Uses of I18n Rest Controller

The i18n interface has a few methods for managing bundles or translations. Next in this example, we will test the bundle values CRUD(Create, Read, Update, Delete) methods.

To simplify the code being written, three dots (…) may appear in some parts of the code. This indicates that there may be previous code before and after those dots.

Query bundle values

To query the bundle values we create a method that receieves the bundle class, the language to query and the country of this language.

The code to query the values of a bundle is in the previous section.

Update bundle values

To update the bundle values we create a method that receieves the translations to modify of a bundle that we have specified.

Code

I18nRestController.java

package com.ontimize.projectwiki.ws.core.rest;

import java.util.ArrayList;
. . .

import com.ontimize.jee.common.gui.i18n.DatabaseBundleValues;
import com.ontimize.jee.server.rest.UpdateParameter;

. . .

@RestController
@RequestMapping("/i18n")
@ComponentScan(basePackageClasses = { com.ontimize.jee.common.services.i18n.II18nService.class })
public class I18nRestController {

	@Autowired
	@Qualifier("I18nService")
	private II18nService iI18nService;

  . . .

	@RequestMapping(value = "/bundle/update", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
	public void updateBundleValues(@RequestBody UpdateParameter parameter) {

		DatabaseBundleValues values = new DatabaseBundleValues(iI18nService.getAvailableLocales());
		String bundleClass = String.valueOf(parameter.getData().get("bundleClass"));
		String key = String.valueOf(parameter.getFilter().get("key"));
		ArrayList e = (ArrayList) parameter.getData().get("bundleValues");
		Map<String, Object> translationValues = new HashMap<>(); 
		for (int i = 0; i < e.size(); i++) {
			Map<String,Object> map = (Map<String, Object>) e.get(i);
			translationValues.put(String.valueOf(map.get("language")), map.get("value"));
		}
		values.addBundleValue(key, bundleClass, translationValues);
		iI18nService.updateBundleValues(values);
	}

}

REST request

The request to execute is the following: localhost:33333/i18n/bundle/update

Element Meaning
localhost:33333 Indicates the host
/i18n Indicates the service to be queried
/bundle/update Indicates the request mapping class

The request types can only be PUT and the request body has to have the following structure:

Element Meaning
key Indicates the key of the value to be translated.
bundleClass Indicates the class name of the bundle specified in the database.
bundleValues Indicates the values of the translation to be modified.

Body request:

{
 "filter":{
     "key": "EMAIL"
 },   
 "data":
 {
     "bundleClass": "i18n.bundle",
     "bundleValues": [
         {
             "language":"es_ES",
             "value":"ejemplo@ejemplo.com"
         },
         {
             "language":"en_US",
             "value":"example@example.com"
         },
         {
             "language":"gl_ES",
             "value":"exemplo@exemplo.com"
         }
     ]
 }
}

Insert bundle values

The method to update the bundle values explained in the previous point also allows creating a new value in the case of the specified value does not exist in the database.

DatabaseI18nEngine.java

. . .
Object key = this.getBundleValueKey(filter);
if (key != null) {
    // update
    filter.put(this.bundleValuesKeyColumn, key);
    this.daoBundleValues.update(hValues, filter);
} else {
    // insert
    hValues.putAll(filter);
    this.daoBundleValues.insert(hValues);
}
. . .

Delete bundle values

To delete the bundle values we create a method that receives the bundle class and the key of the bundle values.

I18nRestController.java

package com.ontimize.projectwiki.ws.core.rest;

import java.util.ArrayList;
. . .

import com.ontimize.jee.common.gui.i18n.DatabaseBundleValues;
import com.ontimize.jee.server.rest.DeleteParameter;

. . .

@RestController
@RequestMapping("/i18n")
@ComponentScan(basePackageClasses = { com.ontimize.jee.common.services.i18n.II18nService.class })
public class I18nRestController {

	@Autowired
	@Qualifier("I18nService")
	private II18nService iI18nService;

  . . .

	@RequestMapping(value = "/bundle/delete", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
	public void deleteBundleValues(@RequestBody DeleteParameter parameter) {
		DatabaseBundleValues values = new DatabaseBundleValues(iI18nService.getAvailableLocales());

		String bundleClass = String.valueOf(parameter.getFilter().get("bundleClass"));
		String key = String.valueOf(parameter.getFilter().get("key"));
		
		values.addBundleValue(key, bundleClass, null);
		iI18nService.deleteBundleValues(values);
	}

}

The request to execute is the following: localhost:33333/i18n/bundle/delete

Element Meaning
localhost:33333 Indicates the host
/i18n Indicates the service to be queried
/bundle/delete Indicates the request mapping class

The request types can only be DELETE and the request body has to have the following structure:

Element Meaning
bundleClass Indicates the class name of the bundle specified in the database.
key Indicates the key of the value to be translated.

Body Request

{
 "filter":{
     "bundleClass": "i18n.bundle",
     "key": "EMAIL"
 }
}