I18n from csv/calc

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)

3 Likes

Did you try to use it with javafx (for labels,…) ?

vs

    @Provides
    public ResourceBundle resources(Locale locale) {
        return ResourceBundle.getBundle("Interface.labels", locale);
    }

    @Provides
    public FXMLLoader fxmlLoader(ResourceBundle resources) {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setResources(resources);
        fxmlLoader.setBuilderFactory(new JavaFXBuilderFactory());
        return fxmlLoader;
    }

As it returns a normal ResourceBundle, it can be plugged right into Javafx as well.

 @Provides
    public ResourceBundle resources(Locale locale) {
        return I18NUtility.getBundle(new File("./translationstest.csv"), locale);
    }