So I got annoyed with having to synchronized several property files by hand. Eg forgetting to put in the english translation after changing my native one ect.
Also property files have some special cases wich needs to be escaped, and to easy later work to let others translate I wanted to get rid of this.
So I wrote a small utility, that allows you to get Ressourcebundles from a csv file.
I18NUtility.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Scanner;
public class I18NUtility {
private static HashMap<File, ClassLoader> processed = new HashMap<>();
private I18NUtility() {
}
public synchronized static ResourceBundle getBundle(final File csv, final Locale local) {
final String csvname = csv.getName().replace(".csv", "");
ClassLoader i18nloader = I18NUtility.processed.get(csv);
if (i18nloader == null) {
try (final Scanner in = new Scanner(csv)) {
// process header
final String[] header = in.nextLine().split(";");
final File[] outFiles = new File[header.length];
final Properties[] languageProperties = new Properties[header.length];
for (int i = 2; i < header.length; i++) {
String language = header[i];
if (!language.isEmpty()) {
language = "_" + language;
}
final File outfile = new File(csv.getParentFile(), csvname + language + ".properties");
outFiles[i] = outfile;
languageProperties[i] = new Properties();
}
// reading to properties
while (in.hasNextLine()) {
final String[] line = in.nextLine().split(";");
final String key = line[0];
for (int i = 2; i < languageProperties.length; i++) {
languageProperties[i].setProperty(key, line[i]);
}
}
// writing
for (int i = 2; i < languageProperties.length; i++) {
languageProperties[i].store(new FileOutputStream(outFiles[i]), "generated from " + csv.getName());
}
final URL[] urls = { csv.getParentFile().toURI().toURL() };
i18nloader = new URLClassLoader(urls);
I18NUtility.processed.put(csv, i18nloader);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
return ResourceBundle.getBundle(csvname, local, i18nloader);
}
}
translationstest.csv
key;comment;;de
welcomemessage;Variable 0:name;Hello {0};Hallo {0}
Testi18nUtility.java
package de.visiongamestudios.shared.i18n;
import java.io.File;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;
public class Testi18nUtility {
public static void main(final String[] args) {
final ResourceBundle bundle = I18NUtility.getBundle(new File("./translationstest.csv"), Locale.GERMANY);
System.out.println(bundle.getString("welcomemessage"));
final MessageFormat formatter = new MessageFormat("");
formatter.applyPattern(bundle.getString("welcomemessage"));
System.out.println(formatter.format(new Object[] { "JME3 Community" }));
}
}
Implementation Notes:
->Generating different property files out of this has the benefit, that user provided translations for non included languages can be done without modifing game provided files (wich my updater would simple revert). Also it allows users to test their translations within the full application to see problems with to large texts, non situation fitting translation ect.
->For my usecase it is easier to have the csv as a real file, as it allows other users to add their own language, and send me the file back, for inclusion in the official version.
->Allows for full functionality of Resourcebundles for translations, like compound messages, fallbacks ect.
→ ; in the text is not directly supported but rarely used in most languages, (possible via utf8 escape code)
→ MS excel often escapes exported text with “” and does not write in UTF-8 I recommend libreoffice calc for this. (also it supports 1 million rows, which should be enough for most games)