This project brings an implementation of Springs MessageSource
interface, which is responsible for resolving texts in an internationalised manner. See the
Spring Reference for more details about Springs i18n mechanism.
Spring currently ships with two implementations of MessageSource
. While StaticMessageSource
is very simple and intended for testing-purposes, ResourceBundleMessageSource
is a layer above JAVAs ResourceBundle
and is mainly used for resolving messages from resource-files like messages_en.properties
.
This project brings another implementation which allows to persist your internationalisation in a RDBMS accessed by JDBC. We think, storing messages in the Database can help you to with several problems:
- Let the user of an application change its translations “on the fly” using the application itself
- Reload messages without Classloading-Issues
- less Encoding-Mess
For those who want to give the project a quick try without learning much about how it works, this section might help.
Include the messagesource-artifact in your Maven build (pom.xml
):
<dependencies>
[...]
<dependency>
<groupId>org.synyx</groupId>
<artifactId>messagesource</artifactId>
<version>0.6.1</version>
</dependency>
</dependencies>
Since the artifact is currently only available from Synyx’ public repositories you have to include this in your POM as well:
<repositories>
[...]
<repository>
<id>nexus.synyx.org</id>
<name>Synyx OpenSource Repository</name>
<url>http://repo.synyx.org</url>
</repository>
</repositories>
Now that your project has the dependency you can integrate it within your applications bean-definition file (maybe applicationContext.xml
or whatever).
Spring searches for a bean named messageSource
within your context and uses this to resolve its messages. So, instead using a classic RessourceBundleMessageSource
backed by a set of .properties
files you define the following which is the minimal configuration of a MessageSource
that reads its data from the database:
<bean id="messageSource" class="org.synyx.messagesource.InitializableMessageSource">
<property name="messageProvider">
<bean class="org.synyx.messagesource.jdbc.JdbcMessageProvider">
<property name="dataSource" ref="dataSource"/>
</bean>
</property>
</bean>
In addition, you need a bean named dataSource
within the context, implementing the javax.sql.DataSource
interface. But you’ll probably have one of those anyway. If your DataSource
-Bean has a different name simply adjust it in the ref
attribute above.
Both beans we define here have a set of properties for additional configuration which are not completely mentioned here for the sake of simplicity. But we will go into more detail in the following sections.
For the above configuration the database where your DataSource
leads to is expected to have a table containing the messages named Messages and the following colums:
- a column named language containing the language-code the message is for
- a column named country containing the country-code the message is for
- a column named variant containing the variant-code the message is for
- a column named basename containing the basename (can be seen as a cateogry) the message is for
- a column named key containing the key-code of the message
- a column named message containing the message itself, including “usual” patterns for placeholders ({0} etc)
All columns must be String-Types (e.g. VARCHAR for mySQL) and may be empty.
The name of the table as well of the columns can be configured using <property name="languageColumn" value="sprache"/>
on JdbcMessageProvider
. Replace language with country, variant, key, message, basename accordingly and use the tableName property to change the table.
From version 0.6.1 on you may also set the delimiter used to delimit the names of columns and the table using <property name="delimiter" value="`"/>
to fit your databases needs. If your database supports this its ok to set this to an empty string or whatever.
To create such a table in your database you could use a statement like the following:
CREATE TABLE `Message` (
`basename` VARCHAR( 31 ) NOT NULL ,
`language` VARCHAR( 7 ) NULL ,
`country` VARCHAR( 7 ) NULL ,
`variant` VARCHAR( 7 ) NULL ,
`key` VARCHAR( 255 ) NULL ,
`message` TEXT NULL
);
If you insert messages into the table, you must set a basename. All the other fields may be empty or null (because null is treated in a special manner in databases you might want to use an empty String for “not set”). If you want to insert a global defaultmessage for a given basename simply leave language, country and variant empty. If you want to insert an english message, just set the language-column to “en” and leave country and variant empty. tbc.
Please note that the values within the columns language, country and variant correspond directly to the values in java.util.Locale
which means they should match the correct ISO-Codes. See @Locale@s API-Doc for details.
Current performance test results : 30000 messages are inserted in 5 seconds with the following configuration.
You only need to add “rewriteBatchedStatements” parameter to database url to achive this throughput.
Test enviorment
Mysql version- 5.1.65
Driver/Connector- mysql-connector-java-5.1.15.jar
Note- Add following “&rewriteBatchedStatements=true” parameter to your jdbc.url
Sample-
Before Replacing – jdbc.url=jdbc:mysql:///?useUnicode=true&characterEncoding=utf8
After Replacing – jdbc.url=jdbc:mysql:///?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
The InitializableMessagesource
kind of works like springs regular ResourceBundleMessageSource
(which delegates to ResourceBundle
) except that it loads all the messages at once using a MessageProvider
.
An InitializableMessagesource
can be responsible for 1 or more basenames which can be seen as “message-categories”. One of these basenames would be equal to to a set of .properties
files with the same basename (matching the pattern basename_language_country_variant.properties) when you’d use ResourceBundleMessageSource
. With InitializableMessagesource
messages are categorized the same way but “stored” however the configured MessageProvider
wants to (e.g. in Database for JDBCMessageProvider
).
As mentioned before, InitializableMessagesource
loads all its messages at certain points. This is every time its initialize()
method is called. If you do not configure it otherwise and use Spring to create your MessageSource
this is also at construction-time. Afterwards you may inject the InitializableMessagesource
into your components and call initialize()
again at any time.
If you do not want the MessageSource
to be initialized on construction-time you may set the autoInitialize
-property to false:
<bean id="messageSource" class="org.synyx.messagesource.InitializableMessageSource">
<property name="autoInitialize" value="false" />
[...]
</bean>
Each time initialize()
is called, the InitializableMessagesource
asks its MessageProvider
for all the messages it has (for the configured basenames). These messages are cached until initialize()
is called the next time.
InitializableMessagesource
uses a MessageProvider
to load its messages. The project currently ships two implementations of this interface. Of course, you can implement your own one too and set it InitializableMessageSource
using its messageProvider
-property.
<bean id="messageSource" class="org.synyx.messagesource.InitializableMessageSource">
<property name="messageProvider"> <bean class="org.synyx.MySpecialMessageProvider"/> </property>
[...]
</bean>
The two implementations provided are FileSystemMessageProvider
and JdbcMessageProvider
.
JdbcMessageProvider
looks up messages from a table in a given database (see above for configuration options, mainly the names of the table and the columns).
FileSystemMessageProvider
behaves kind of like the known ResourceBundleMessageSource
, except that its aware of all .properties
files in a directory. You configure it by giving it a File
or String
leading to the directory where it looks for files with the patern *.properties while being aware of the “locale-postfixes” within the filename. Using this alone will probably not solve any bigger problems for you, but it is very useful when it comes to importing messages from a .properties
file into the database and vice-versa.
By default, InitializableMessageSource
first asks its MessageProvider
for all available basenames and then requests the messages for each basename returned.
You may limit this by using the basename
or basenames
properties. If you want the MessageSource
to be responsible only for a single basename, use the basename property.
<bean id="messageSource" class="org.synyx.messagesource.InitializableMessageSource">
<property name="basenames">messages</property>
[...]
</bean>
If you want to limit it to more than one basename, use basenames.
<bean id="messageSource" class="org.synyx.messagesource.InitializableMessageSource">
<property name="basenames">
<list>
<value>messages</value>
<value>special</value>
</list>
</property>
[...]
</bean>
As mentioned, if you neither set the basename nor the basenames properties, the InitializableMessageSource
simply resolves messages for all the basenames available from the MessageProvider
, which is usually a good way to start. In this case you are able to add new categories (basenames) on the fly by simply inserting corresponding rows into the database.
Sample spring configuration
returnUnresolvedCode-If set true returns message code (key), if no message could be resolved for this code. Helps avoiding null pointer exception at UI level.
defaultLocale- This locale will be explicitly set for the message, if the message is resolved from a base name without locale(usually default basename file).This parameter helps avoid null pointer exception for the messages with arguments.
Example- test.message= hi{0}, welcome back.
will give error if the message is resolved from the basename file without locale specied in it name.
InitializableMessagesource
resolves its messages the same way ResourceBundle
does.
When resolving a message the keys are tried to resolve in the following order:
- given Locales language + country + variant
- given Locales language + country
- given Locales language
- default Locales language + country + variant (property defaultLocale, if not null)
- default Locales language + country (property defaultLocale, if not null)
- default Locales language (property defaultLocale, if not null)
- Global Default (basename)
As you can see, there is (like in ResourceBundle
) a defaultLocale involved. Unlike with ResourceBundle
this is not the Systems default but one you may set explicitly using the defaultLocale
-property of InitializableMessagesource
. If you do not set it (or set it to null
) the MessageSource
falls back to the global default for the basename skipping the default-steps mentioned above.
<bean id="messageSource" class="org.synyx.messagesource.InitializableMessageSource">
<property name="defaultLocale" value="en_US" />
[...]
</bean>
If you want to import an existing set of ResourceBundle
files you might want to give the Importer
a try. JdbcMessageProvider
and FileSystemMessageProvider
also implement a second interface: MessageAcceptor
which can be used to set messages (Save them to the filesystem or database).
Example:
@Autowired
private JdbcMessageProvider jdbcMessageProvider;
MessageProvider source = filesystemMessageProvider;
MessageAcceptor target = jdbcMessageProvider;
Importer importer = new Importer(source, target);
// imports messages of all basenames from source to target
importer.importMessages();
// or just import some basenames?
Collection<String> basenames = source.getAvailableBaseNames();
for (String basename : basenames) {
if (decideIfImport(basename)) {
importer.importMessages(basename);
}
}
The same way this works for importing from filesystem to database you may also “export” from database to filesystem by switching source and target.
If you prefer to get your .properties
files zipped you may use the ZipMessageAcceptor
which writes the files zip-compressed to an OutputStream
or File
. Reading .properties
files from ZipMessageAcceptor
is currently not supported (ZipMessageAcceptor
only implements the MessageAcceptor
interface, not the MessageProvider
).