Skip to content

Developer Guide

This section covers the data model, configuration reference, caching strategy, and extension points available to developers integrating I18N Manager into an application.

Data Model

I18N Manager persists three related entities. A language defines a locale available in the application. A key is a unique identifier for a translatable string. A translation links a key to a language and holds the translated value.

---
config:
  layout: elk
---
erDiagram
    IM_LANGUAGE {
        integer id               PK
        varchar language_key
        varchar language_region
        boolean is_default_language
    }
    IM_KEY {
        integer id          PK
        varchar item_key
        varchar description
    }
    IM_TRANSLATION {
        integer id           PK
        varchar translation
        integer fk_item_key  FK
        integer fk_language  FK
    }

    IM_KEY      ||--o{ IM_TRANSLATION : "translated by"
    IM_LANGUAGE ||--o{ IM_TRANSLATION : "used in"
Hold "Alt" / "Option" to enable pan & zoom

Database Schema

I18N Manager manages three tables. The schema below represents the DDL generated by Hibernate for a standard relational database.

CREATE SEQUENCE im_language_seq
    START WITH 1 INCREMENT BY 50;

CREATE SEQUENCE im_key_seq
    START WITH 1 INCREMENT BY 50;

CREATE SEQUENCE im_translation_seq
    START WITH 1 INCREMENT BY 50;

CREATE TABLE im_language (
    id                   INTEGER      NOT NULL,
    language_key         VARCHAR(255) NOT NULL,
    language_region      VARCHAR(255) NOT NULL,
    is_default_language  BOOLEAN,
    CONSTRAINT pk_im_language             PRIMARY KEY (id),
    CONSTRAINT uq_im_language_key_region  UNIQUE (language_key, language_region)
);

CREATE TABLE im_key (
    id           INTEGER      NOT NULL,
    item_key     VARCHAR(255),
    description  VARCHAR(255),
    CONSTRAINT pk_im_key  PRIMARY KEY (id),
    CONSTRAINT uq_im_key  UNIQUE (item_key)
);

CREATE TABLE im_translation (
    id           INTEGER       NOT NULL,
    translation  VARCHAR(4000),
    fk_item_key  INTEGER,
    fk_language  INTEGER,
    CONSTRAINT pk_im_translation           PRIMARY KEY (id),
    CONSTRAINT fk_im_translation_key       FOREIGN KEY (fk_item_key) REFERENCES im_key (id),
    CONSTRAINT fk_im_translation_language  FOREIGN KEY (fk_language) REFERENCES im_language (id)
);

The language_key column stores an ISO 639-1 language code (e.g., en, es). The language_region column stores an ISO 3166-1 alpha-2 region code (e.g., US, GB) or an empty string for a non-regional language. The unique constraint on (language_key, language_region) prevents duplicate locale definitions. The translation column holds up to 4000 characters, so a translation can contain a short explanatory paragraph (4000 is the largest length portable across databases, matching Oracle's VARCHAR2 limit). The item_key column is limited by the configurable com.appjars.i18nmanager.key.max-length property (default 255).

Note

The three tables share the im_ prefix. Databases created by earlier releases of I18N Manager carry the previous aj_i18n_ prefix instead, because Hibernate does not rename existing tables. Adjust the table names in the scripts below to match the deployment.

Database Notes

Long translation values

The translation column of im_translation is mapped with a length of 4000, which maps to varchar(4000) on PostgreSQL, MySQL, and H2.

Hibernate's hibernate.hbm2ddl.auto=update does not widen existing columns, so this length only applies to databases created from the current version onward. Widen the column manually on an existing database:

-- PostgreSQL
ALTER TABLE im_translation ALTER COLUMN translation TYPE varchar(4000);

-- MySQL
ALTER TABLE im_translation MODIFY translation VARCHAR(4000);

On PostgreSQL, altering the column while the application is running may produce cached plan must not change result type errors until the pooled connections are recycled. Restart the application, or recycle the connection pool, once after running the statement.

MySQL: required setup

MySQL ships with case-insensitive defaults for utf8mb4, such as utf8mb4_0900_ai_ci on MySQL 8. Under that collation, the item_key column of im_key treats two keys that differ only in letter case as the same value, so the unique constraint rejects the second one. In Java those are two distinct keys with potentially distinct translations, so I18N Manager does not merge them: the affected keys are reported as skipped keys instead.

Run the following statement once against the schema, after Hibernate has created the tables on the first application start:

ALTER TABLE im_key
  MODIFY item_key VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

The recommended sequence for a first deployment on MySQL is:

  1. Start the application once so that Hibernate creates im_key and the related tables.
  2. Stop the application.
  3. Run the ALTER TABLE statement above.
  4. Start the application again, then import translations through Scan missing keys or a .properties upload.

PostgreSQL, H2, and Oracle deployments using a _bin or _cs collation compare text case-sensitively by default and do not need this statement.

Module Overview

I18N Manager is structured as six Maven modules following the AppJars layered architecture:

Module Artifact ID Description
Model appjars-i18n-manager-model DTOs, filter classes, auto-configuration, cache setup
Business API appjars-i18n-manager-business LanguageService, I18nKeyService, TranslationItemService, LocaleService interfaces
Business Impl appjars-i18n-manager-business-impl Service implementations with caching and validation
Data API appjars-i18n-manager-data LanguageDao, I18nKeyDao, TranslationItemDao interfaces
Data Impl appjars-i18n-manager-data-impl JPA entities, DAO implementations, Spring Data repositories
Flow UI appjars-i18n-manager-flow Vaadin views, VaadinI18nProvider, route configuration

A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.

Spring Auto-Configuration

I18N Manager registers itself through Spring Boot's auto-configuration mechanism. The entry point is I18nManagerAutoConfiguration, declared in:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

The class scans components and entities under the package of I18nManagerModule. It does not enable the Spring Data repositories, so the host application must declare @EnableJpaRepositories itself. See Getting started.

In addition, the auto-configuration enables Spring caching (@EnableCaching) and registers a ConcurrentMapCacheManager bean with three named caches:

Cache name Purpose
im_languages Language lookups, default language, and locale list
im_keys Individual translation key lookups
im_translations Translation lookups by key and locale

All service mutations evict the relevant cache entries. Bulk operations evict entire cache regions. The in-memory cache provided by default is suitable for single-node deployments. Distributed cache managers can be substituted by registering a compatible CacheManager bean.

Configuration Properties

The following properties can be set in application.properties to customise the behaviour of I18N Manager. Every property has a default, so none of them has to be set.

View URLs

Property Default Description
com.appjars.i18nmanager.url.views.languages i18n/languages URL path of the language management view
com.appjars.i18nmanager.url.views.translationitems i18n/items URL path of the translation items view
com.appjars.i18nmanager.url.views.translationitemsparams i18n/items/:lang URL path of the translation items view with a pre-selected language parameter

Translation keys and resource bundles

Property Default Description
com.appjars.i18nmanager.key.max-length 255 Maximum character length for translation keys, enforced by the service layer and by the key field in the translation items view
com.appjars.i18nmanager.properties.encoding UTF-8 Charset used to decode the bundled messages_<locale>.properties files when scanning for missing keys or uploading values (e.g. UTF-8, ISO-8859-1)

Note

The JDK default for Properties.load(InputStream) is ISO-8859-1. I18N Manager defaults to UTF-8 instead. Set com.appjars.i18nmanager.properties.encoding=ISO-8859-1 only for resource bundles that rely on the legacy decoding.

Locale resolution

Property Default Description
com.appjars.i18nmanager.default-locale en Locale used as the final fallback when no matching translation is found
com.appjars.i18nmanager.auto-resolve-locale false When true, re-resolves the session locale from the browser's Accept-Language header on every UI initialisation
com.appjars.utils.i8n.supportedLocales en Comma-separated list of locale tags whose resource bundles are preloaded

Note

com.appjars.utils.i8n.supportedLocales is not an I18N Manager property. It is declared by appjars-utils and shared with the other AppJars, which is why it carries a different prefix. The i8n transposition is part of the actual property name.

Translation Lookup and Fallback

When a translation is requested for a given key and locale, I18N Manager resolves it in this order:

  1. Regional translation — the translation for the exact locale (e.g., en_US).
  2. Non-regional fallback — if no regional translation exists, the translation for the same language without a region (e.g., en with an empty region).
  3. Default language — if neither of the above exists, the translation for the language marked as default.

If no translation is found at any of these levels, the lookup falls through to the classpath resource bundles and, failing that, returns an empty string.

The three database levels are resolved by a single query with three LEFT JOINs, making lookups efficient regardless of how many fallback levels are traversed.

Locale Resolution

The locale a Vaadin session runs under determines which translations are served. By default, Vaadin resolves it once when the session is created.

Setting com.appjars.i18nmanager.auto-resolve-locale=true registers LocaleAutoResolver, which re-resolves the locale on every UI initialisation — so a page reload picks up a language that was added through the UI after the session started. This is useful precisely because I18N Manager creates languages at runtime.

flowchart TD
    A[UI initialised] --> B{Manual locale<br/>set for session?}
    B -- yes --> Z[Keep session locale]
    B -- no --> C{Any provided<br/>locales?}
    C -- no --> Z
    C -- yes --> D[Read Accept-Language]
    D --> E{Exact match in<br/>provided locales?}
    E -- yes --> Y[Set session locale]
    E -- no --> F{Same language,<br/>any region?}
    F -- yes --> Y
    F -- no --> G[Use provider default locale]
    G --> Y
Hold "Alt" / "Option" to enable pan & zoom

When a single language is registered, that language is always used and the header is not consulted.

To let a user pick a language explicitly, set it through LocaleAutoResolver rather than on the session directly. Doing so marks the session as manually set, which suppresses automatic resolution on subsequent page loads:

// Apply an explicit user choice and stop auto-resolving for this session
LocaleAutoResolver.setManualLocale(VaadinSession.getCurrent(), Locale.of("es", "AR"));

// Resume automatic resolution on the next UI initialisation
LocaleAutoResolver.clearManualLocale(VaadinSession.getCurrent());

Vaadin I18n Provider

I18N Manager provides VaadinI18nProvider, a @Primary Spring bean that replaces the standard Vaadin i18n provider. When the -flow module is on the classpath, all calls to getTranslation() in Vaadin components are automatically routed through the database-backed translation service. No configuration is required to activate it.

VaadinI18nProvider integrates with the fallback hierarchy described above and supports MessageFormat parameter substitution, so translation strings may contain placeholders such as {0} and {1}. A stored value that is not a valid MessageFormat pattern is returned verbatim rather than raising an error.

The set of locales reported to Vaadin as supported is driven by LanguageService.getProvidedLocales(), which returns all languages stored in the database. This means the locale list is dynamic and updates as languages are added or removed through the UI.

Free-Tier Limits

Without a full license, I18N Manager enforces two limits:

Limit Value
Languages 2
Translation items 100

The translation-item limit is counted per language, and excludes keys under appjars. — the translation keys belonging to the AppJars themselves do not consume an application's free allowance.

Both limits are enforced in the service layer and reflected in the UI: the views disable the actions that would exceed a limit and display a restrictions bar with live count badges. If a database already holds more rows than a limit allows — for example after a license expires — the views additionally disable editing altogether. The user-facing behaviour is described in Language Management and Translation Items.

Service API

LanguageService

Manages language definitions. Key methods beyond standard CRUD:

// Return the language marked as default
LanguageDto getDefaultLanguage();

// Set a language as the default (clears the previous default)
void setDefaultLanguage(LanguageDto language);

// Find a language by its key and region
Optional<LanguageDto> findByLanguageKeyAndRegion(String languageKey, String languageRegion);

// Find the non-regional entry for a language key (region = "")
Optional<LanguageDto> findNonRegionalLanguage(String languageKey);

// Return all languages as Java Locale objects
List<Locale> getProvidedLocales();

// Export all translations for a language as a Properties object
Properties generateMessagePropertiesForLanguage(LanguageDto language);

I18nKeyService

Manages translation keys. Key methods beyond standard CRUD:

// Find a key by its string value
Optional<I18nKeyDto> findByKey(String key);

// Load keys and translations from a Properties object; returns the keys that were skipped
List<String> persistAll(Properties properties, LanguageDto language, boolean overwrite);

// Load keys and translations from an InputStream (e.g., a .properties file)
List<String> persistAll(InputStream inputStream, LanguageDto language, boolean overwrite)
    throws IOException;

// Count keys matching a partial or exact key value
long count(String keyItem, StringComparison comparison);

// Bulk delete keys and their associated translations
void deleteAll(Collection<I18nKeyDto> keys);

The persistAll methods are the programmatic equivalent of the upload action in the UI. When overwrite is false, existing translations are preserved and only missing keys are added.

Both overloads return the list of keys the database refused as duplicates of an existing row, which happens on a case-insensitive collation. The list is empty on a successful load. The UI surfaces it as the Skipped keys dialog; a programmatic caller should log or otherwise report it, because those keys were not stored.

TranslationItemService

Manages individual translation values. Key methods beyond standard CRUD:

// Look up a translation for a key and locale, applying the fallback hierarchy
Optional<String> getTranslation(String itemKey, Locale locale);

// Save or delete a translation (an empty value triggers deletion)
void process(TranslationItemDto item);

// Bulk save or delete translations
void processAll(Collection<TranslationItemDto> items);

// Create a key and its translation in a single transaction; returns the new key id
Integer saveKeyAndTranslation(TranslationItemDto entity);

// Update a key description and its translation in a single transaction
void updateKeyAndTranslation(TranslationItemDto entity);

// Query keys with their translation for a language, plus the fallback and default values
Stream<TranslationDto> findByFilter(TranslationItemFilter filter);

// Same query, returning a single result
Optional<TranslationDto> findSingleByFilter(TranslationItemFilter filter);

// Count the results of a filter without materialising them
long countByFilter(TranslationItemFilter filter);

// Load the translations of several keys for one language
List<TranslationItemDto> findByItemKeysAndLanguage(Collection<String> keyItems, Integer langId);

// Delete all translations associated with a key
void deleteByItemKey(I18nKeyDto key);

// Delete all translations associated with several keys
void deleteByItemKeys(Collection<I18nKeyDto> keys);

// Delete all translations for a language
void deleteByLanguage(Integer langId);

getTranslation() is the method called by VaadinI18nProvider for every UI string lookup. Results are cached in im_translations.

saveKeyAndTranslation and updateKeyAndTranslation exist so that a key and its translation are committed together. Calling the key and translation services separately can leave a key with no translation if the second call fails.

LocaleService

Provides utilities for working with Java Locale objects independently of stored data:

// Return the localised display name of a language
String getDisplayLanguage(Locale targetLocale, String languageKey);

// Same, with the target locale given as a locale key
String getDisplayLanguage(String targetLocaleKey, String languageKey);

// Return all ISO 639-1 language codes
List<String> getAllLocaleKeys();

// Return every language display name, localised for the given locale
List<String> getAllLangNamesInLocale(Locale targetLocale);

Programmatic Translation Import

Translations can be loaded programmatically from standard Java .properties files at application startup. This is useful for seeding an empty database with a base translation set:

@Component
public class TranslationSeeder {

    private static final Logger logger = LoggerFactory.getLogger(TranslationSeeder.class);

    @Autowired private LanguageService languageService;
    @Autowired private I18nKeyService i18nKeyService;

    @EventListener(ContextRefreshedEvent.class)
    public void seed() throws IOException {
        Optional<LanguageDto> english = languageService.findByLanguageKeyAndRegion("en", "");
        if (english.isPresent()) {
            try (InputStream is = getClass().getResourceAsStream("/messages_en.properties")) {
                List<String> skipped = i18nKeyService.persistAll(is, english.get(), false);
                if (!skipped.isEmpty()) {
                    logger.warn("Keys not stored: {}", skipped);
                }
            }
        }
    }
}

Setting overwrite to false ensures that manual edits made through the UI are not reverted on each startup.

Customisation

Assigning a Router Layout

By default, I18N Manager registers its views without a parent layout. To wrap them in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:

@Autowired
@Qualifier("I18nManagerRouteConfigurer")
private RouteConfigurer routeConfigurer;

@PostConstruct
public void configure() {
    routeConfigurer.setViewsRouterLayout(MainLayout.class);
}

The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.

When the views must be nested in more than one layout, build the chain instead. addViewsLayoutChain appends one layout at a time, and setViewsLayoutChain replaces the whole chain. The layout set through setViewsRouterLayout is appended after the chain, so the two can be combined:

@PostConstruct
public void configure() {
    routeConfigurer.addViewsLayoutChain(OuterLayout.class);
    routeConfigurer.setViewsRouterLayout(MainLayout.class);
}

getViewsLayoutChain returns an unmodifiable view of the chain, so the chain can only be changed through these methods.

Customising View URLs

The default URL paths can be overridden in application.properties:

com.appjars.i18nmanager.url.views.languages=myapp/languages
com.appjars.i18nmanager.url.views.translationitems=myapp/translations
com.appjars.i18nmanager.url.views.translationitemsparams=myapp/translations/:lang