diff --git a/legacyworlds-server-DIST/pom.xml b/legacyworlds-server-DIST/pom.xml
index a2fc403..fc85884 100644
--- a/legacyworlds-server-DIST/pom.xml
+++ b/legacyworlds-server-DIST/pom.xml
@@ -21,7 +21,6 @@
 		<dependency>
 			<groupId>com.deepclone.lw</groupId>
 			<artifactId>legacyworlds-server-main</artifactId>
-			<version>${project.version}</version>
 		</dependency>
 	</dependencies>
 
diff --git a/legacyworlds-server-DIST/src/server.xml b/legacyworlds-server-DIST/src/server.xml
index 20a5255..875f9b7 100644
--- a/legacyworlds-server-DIST/src/server.xml
+++ b/legacyworlds-server-DIST/src/server.xml
@@ -56,8 +56,8 @@
 			<directory>../legacyworlds-server-main/data</directory>
 			<outputDirectory>data</outputDirectory>
 			<includes>
-				<include>*.txt</include>
-				<include>*.xml</include>
+				<include>i18n/**</include>
+				<include>**.xml</include>
 			</includes>
 		</fileSet>
 
diff --git a/legacyworlds-server-main/data/i18n-text.xml b/legacyworlds-server-main/data/i18n-text.xml
index c2ef4e9..b148a45 100644
--- a/legacyworlds-server-main/data/i18n-text.xml
+++ b/legacyworlds-server-main/data/i18n-text.xml
@@ -1,23 +1,22 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text"
-xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text
-i18n-text.xsd">
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text i18n-text.xsd">
 
 	<language id="en" name="English">
-		<from-file id="registrationMail" source="data/registrationMail-en.txt" />
-		<from-file id="passwordRecoveryMail" source="data/passwordRecoveryMail-en.txt" />
-		<from-file id="reactivationMail" source="data/reactivationMail-en.txt" />
-		<from-file id="addressChangeMail" source="data/addressChangeMail-en.txt" />
-		<from-file id="adminRecapMail" source="data/adminRecapMail.txt" />
-		<from-file id="messageMail" source="data/messageMail-en.txt" />
-		<from-file id="recapMail" source="data/recapMail-en.txt" />
-		<from-file id="quitMail" source="data/quitMail-en.txt" />
-		<from-file id="bannedMail" source="data/bannedMail-en.txt" />
-		<from-file id="banLiftedMail" source="data/banLiftedMail-en.txt" />
-		<from-file id="adminErrorMail" source="data/adminErrorMail.txt" />
-		<from-file id="inactivityWarningMail" source="data/inactivityWarningMail-en.txt" />
-		<from-file id="inactivityQuitMail" source="data/inactivityQuitMail-en.txt" />
+		<from-file id="registrationMail" source="i18n/en/registrationMail.txt" />
+		<from-file id="passwordRecoveryMail" source="i18n/en/passwordRecoveryMail.txt" />
+		<from-file id="reactivationMail" source="i18n/en/reactivationMail.txt" />
+		<from-file id="addressChangeMail" source="i18n/en/addressChangeMail.txt" />
+		<from-file id="adminRecapMail" source="i18n/adminRecapMail.txt" />
+		<from-file id="messageMail" source="i18n/en/messageMail.txt" />
+		<from-file id="recapMail" source="i18n/en/recapMail.txt" />
+		<from-file id="quitMail" source="i18n/en/quitMail.txt" />
+		<from-file id="bannedMail" source="i18n/en/bannedMail.txt" />
+		<from-file id="banLiftedMail" source="i18n/en/banLiftedMail.txt" />
+		<from-file id="adminErrorMail" source="i18n/adminErrorMail.txt" />
+		<from-file id="inactivityWarningMail" source="i18n/en/inactivityWarningMail.txt" />
+		<from-file id="inactivityQuitMail" source="i18n/en/inactivityQuitMail.txt" />
 		
 		<inline-string id="instantNotification">
 			<value>
@@ -518,19 +517,20 @@ It was disbanded.</value>
 	</language>
 
 	<language id="fr" name="Français">
-		<from-file id="registrationMail" source="data/registrationMail-fr.txt" />
-		<from-file id="passwordRecoveryMail" source="data/passwordRecoveryMail-fr.txt" />
-		<from-file id="reactivationMail" source="data/reactivationMail-fr.txt" />
-		<from-file id="addressChangeMail" source="data/addressChangeMail-fr.txt" />
-		<from-file id="adminRecapMail" source="data/adminRecapMail.txt" />
-		<from-file id="messageMail" source="data/messageMail-fr.txt" />
-		<from-file id="recapMail" source="data/recapMail-fr.txt" />
-		<from-file id="quitMail" source="data/quitMail-fr.txt" />
-		<from-file id="bannedMail" source="data/bannedMail-fr.txt" />
-		<from-file id="banLiftedMail" source="data/banLiftedMail-fr.txt" />
-		<from-file id="adminErrorMail" source="data/adminErrorMail.txt" />
-		<from-file id="inactivityWarningMail" source="data/inactivityWarningMail-fr.txt" />
-		<from-file id="inactivityQuitMail" source="data/inactivityQuitMail-fr.txt" />
+		<from-file id="registrationMail" source="i18n/fr/registrationMail.txt" />
+		<from-file id="passwordRecoveryMail" source="i18n/fr/passwordRecoveryMail.txt" />
+		<from-file id="reactivationMail" source="i18n/fr/reactivationMail.txt" />
+		<from-file id="addressChangeMail" source="i18n/fr/addressChangeMail.txt" />
+		<from-file id="messageMail" source="i18n/fr/messageMail.txt" />
+		<from-file id="recapMail" source="i18n/fr/recapMail.txt" />
+		<from-file id="quitMail" source="i18n/fr/quitMail.txt" />
+		<from-file id="bannedMail" source="i18n/fr/bannedMail.txt" />
+		<from-file id="banLiftedMail" source="i18n/fr/banLiftedMail.txt" />
+		<from-file id="inactivityWarningMail" source="i18n/fr/inactivityWarningMail.txt" />
+		<from-file id="inactivityQuitMail" source="i18n/fr/inactivityQuitMail.txt" />
+
+		<from-file id="adminRecapMail" source="i18n/adminRecapMail.txt" />
+		<from-file id="adminErrorMail" source="i18n/adminErrorMail.txt" />
 
 		<inline-string id="instantNotification">
 			<value>
diff --git a/legacyworlds-server-main/data/adminErrorMail.txt b/legacyworlds-server-main/data/i18n/adminErrorMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/adminErrorMail.txt
rename to legacyworlds-server-main/data/i18n/adminErrorMail.txt
diff --git a/legacyworlds-server-main/data/adminRecapMail.txt b/legacyworlds-server-main/data/i18n/adminRecapMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/adminRecapMail.txt
rename to legacyworlds-server-main/data/i18n/adminRecapMail.txt
diff --git a/legacyworlds-server-main/data/addressChangeMail-en.txt b/legacyworlds-server-main/data/i18n/en/addressChangeMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/addressChangeMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/addressChangeMail.txt
diff --git a/legacyworlds-server-main/data/banLiftedMail-en.txt b/legacyworlds-server-main/data/i18n/en/banLiftedMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/banLiftedMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/banLiftedMail.txt
diff --git a/legacyworlds-server-main/data/bannedMail-en.txt b/legacyworlds-server-main/data/i18n/en/bannedMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/bannedMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/bannedMail.txt
diff --git a/legacyworlds-server-main/data/inactivityQuitMail-en.txt b/legacyworlds-server-main/data/i18n/en/inactivityQuitMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/inactivityQuitMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/inactivityQuitMail.txt
diff --git a/legacyworlds-server-main/data/inactivityWarningMail-en.txt b/legacyworlds-server-main/data/i18n/en/inactivityWarningMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/inactivityWarningMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/inactivityWarningMail.txt
diff --git a/legacyworlds-server-main/data/messageMail-en.txt b/legacyworlds-server-main/data/i18n/en/messageMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/messageMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/messageMail.txt
diff --git a/legacyworlds-server-main/data/passwordRecoveryMail-en.txt b/legacyworlds-server-main/data/i18n/en/passwordRecoveryMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/passwordRecoveryMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/passwordRecoveryMail.txt
diff --git a/legacyworlds-server-main/data/quitMail-en.txt b/legacyworlds-server-main/data/i18n/en/quitMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/quitMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/quitMail.txt
diff --git a/legacyworlds-server-main/data/reactivationMail-en.txt b/legacyworlds-server-main/data/i18n/en/reactivationMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/reactivationMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/reactivationMail.txt
diff --git a/legacyworlds-server-main/data/recapMail-en.txt b/legacyworlds-server-main/data/i18n/en/recapMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/recapMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/recapMail.txt
diff --git a/legacyworlds-server-main/data/registrationMail-en.txt b/legacyworlds-server-main/data/i18n/en/registrationMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/registrationMail-en.txt
rename to legacyworlds-server-main/data/i18n/en/registrationMail.txt
diff --git a/legacyworlds-server-main/data/addressChangeMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/addressChangeMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/addressChangeMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/addressChangeMail.txt
diff --git a/legacyworlds-server-main/data/banLiftedMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/banLiftedMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/banLiftedMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/banLiftedMail.txt
diff --git a/legacyworlds-server-main/data/bannedMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/bannedMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/bannedMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/bannedMail.txt
diff --git a/legacyworlds-server-main/data/inactivityQuitMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/inactivityQuitMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/inactivityQuitMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/inactivityQuitMail.txt
diff --git a/legacyworlds-server-main/data/inactivityWarningMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/inactivityWarningMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/inactivityWarningMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/inactivityWarningMail.txt
diff --git a/legacyworlds-server-main/data/messageMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/messageMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/messageMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/messageMail.txt
diff --git a/legacyworlds-server-main/data/passwordRecoveryMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/passwordRecoveryMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/passwordRecoveryMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/passwordRecoveryMail.txt
diff --git a/legacyworlds-server-main/data/quitMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/quitMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/quitMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/quitMail.txt
diff --git a/legacyworlds-server-main/data/reactivationMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/reactivationMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/reactivationMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/reactivationMail.txt
diff --git a/legacyworlds-server-main/data/recapMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/recapMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/recapMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/recapMail.txt
diff --git a/legacyworlds-server-main/data/registrationMail-fr.txt b/legacyworlds-server-main/data/i18n/fr/registrationMail.txt
similarity index 100%
rename from legacyworlds-server-main/data/registrationMail-fr.txt
rename to legacyworlds-server-main/data/i18n/fr/registrationMail.txt
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java
index c3952ed..8d6e856 100644
--- a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/ImportText.java
@@ -1,9 +1,7 @@
 package com.deepclone.lw.cli;
 
 
-import java.io.*;
-import java.util.LinkedList;
-import java.util.List;
+import java.io.File;
 
 import javax.sql.DataSource;
 
@@ -12,230 +10,186 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.support.AbstractApplicationContext;
 import org.springframework.context.support.ClassPathXmlApplicationContext;
 import org.springframework.context.support.FileSystemXmlApplicationContext;
-import org.springframework.jdbc.core.SqlParameter;
-import org.springframework.jdbc.core.simple.SimpleJdbcCall;
 import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.TransactionStatus;
 import org.springframework.transaction.support.TransactionCallback;
 import org.springframework.transaction.support.TransactionTemplate;
 
-import com.thoughtworks.xstream.XStream;
-import com.thoughtworks.xstream.annotations.XStreamAlias;
-import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
-import com.thoughtworks.xstream.annotations.XStreamImplicit;
+import com.deepclone.lw.cli.xmlimport.I18NLoader;
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText;
+import com.deepclone.lw.cli.xmlimport.data.i18n.LanguageDefinition;
+import com.deepclone.lw.cli.xmlimport.data.i18n.StringDefinition;
+import com.deepclone.lw.utils.StoredProc;
 
 
 
+/**
+ * I18N text import tool
+ * 
+ * <p>
+ * This class defines the body of the I18N text import tool. It loads the data file specified on the
+ * command line, validates it, then proceeds to loading the languages and strings to the database.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ * 
+ */
 public class ImportText
 		extends CLITool
 {
 
+	/** Logging system */
 	private final Logger logger = Logger.getLogger( ImportText.class );
 
-	@SuppressWarnings( "serial" )
-	public abstract static class StringData
-			implements Serializable
-	{
-		@XStreamAsAttribute
-		public String id;
-
-
-		public abstract String getString( );
-	}
-
-	@SuppressWarnings( "serial" )
-	@XStreamAlias( "inline-string" )
-	public static class InlineString
-			extends StringData
-	{
-		public String value;
-
-
-		@Override
-		public String getString( )
-		{
-			return this.value;
-		}
-	}
-
-	@SuppressWarnings( "serial" )
-	@XStreamAlias( "from-file" )
-	public static class FileString
-			extends StringData
-	{
-		@XStreamAsAttribute
-		public String source;
-
-
-		@Override
-		public String getString( )
-		{
-			StringBuilder sBuilder = new StringBuilder( );
-			try {
-				BufferedReader in = new BufferedReader( new FileReader( source ) );
-				String str;
-				while ( ( str = in.readLine( ) ) != null ) {
-					sBuilder.append( str );
-					sBuilder.append( "\n" );
-				}
-				in.close( );
-			} catch ( IOException e ) {
-				throw new RuntimeException( "Could not read " + source );
-			}
-
-			return sBuilder.toString( );
-		}
-	}
-
-	@SuppressWarnings( "serial" )
-	public static class LanguageData
-			implements Serializable
-	{
-		@XStreamAsAttribute
-		public String id;
-
-		@XStreamAsAttribute
-		public String name;
-
-		@XStreamImplicit
-		public List< StringData > strings = new LinkedList< StringData >( );
-	}
-
-	@SuppressWarnings( "serial" )
-	@XStreamAlias( "lw-text-data" )
-	public static class TextData
-			implements Serializable
-	{
-		@XStreamImplicit( itemFieldName = "language" )
-		public List< LanguageData > languages = new LinkedList< LanguageData >( );
-	}
-
+	/** File to read the definitions from */
 	private File file;
+
+	/** Spring transaction template */
 	private TransactionTemplate tTemplate;
-	private SimpleJdbcCall uocTranslation;
-	private SimpleJdbcCall uocLanguage;
-
-
-	private XStream initXStream( )
-	{
-		XStream xstream = new XStream( );
-		xstream.processAnnotations( TextData.class );
-		xstream.processAnnotations( InlineString.class );
-		xstream.processAnnotations( FileString.class );
-		return xstream;
-	}
-
-
-	private TextData loadData( )
-	{
-		FileInputStream fis;
-		try {
-			fis = new FileInputStream( this.file );
-		} catch ( FileNotFoundException e ) {
-			return null;
-		}
-
-		try {
-			XStream xstream = this.initXStream( );
-			return (TextData) xstream.fromXML( fis );
-		} catch ( Exception e ) {
-			e.printStackTrace( );
-			return null;
-		} finally {
-			try {
-				fis.close( );
-			} catch ( IOException e ) {
-				// EMPTY
-			}
-		}
-	}
+
+	/** Language creation or update stored procedure */
+	private StoredProc uocLanguage;
+	/** Translation creation or update stored procedure */
+	private StoredProc uocTranslation;
 
 
+	/**
+	 * Create the Spring context
+	 * 
+	 * <p>
+	 * Load the definition of the data source as a Spring context, then adds the transaction
+	 * management component.
+	 * 
+	 * @return the Spring application context
+	 */
 	private ClassPathXmlApplicationContext createContext( )
 	{
-		// Load data source and Hibernate properties
-		String[] dataConfig = {
-			this.getDataSource( ) ,
-		};
-		FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( dataConfig );
+		FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext( new String[] {
+			this.getDataSource( )
+		} );
 		ctx.refresh( );
 
-		// Load transaction manager bean
-		String[] cfg = {
+		return new ClassPathXmlApplicationContext( new String[] {
 			"configuration/transaction-bean.xml"
-		};
-		return new ClassPathXmlApplicationContext( cfg , true , ctx );
+		} , true , ctx );
 	}
 
 
+	/**
+	 * Create database access templates
+	 * 
+	 * <p>
+	 * Initialise the transaction template as well as both stored procedure definitions.
+	 * 
+	 * @param ctx
+	 *            the Spring application context
+	 */
 	private void createTemplates( ApplicationContext ctx )
 	{
 		DataSource dSource = ctx.getBean( DataSource.class );
 		PlatformTransactionManager tManager = ctx.getBean( PlatformTransactionManager.class );
 
-		this.uocLanguage = new SimpleJdbcCall( dSource ).withCatalogName( "defs" ).withProcedureName( "uoc_language" );
-		this.uocLanguage.withoutProcedureColumnMetaDataAccess( );
-		this.uocLanguage.declareParameters( new SqlParameter( "lid" , java.sql.Types.VARCHAR ) , new SqlParameter(
-				"lname" , java.sql.Types.VARCHAR ) );
+		this.uocLanguage = new StoredProc( dSource , "defs" , "uoc_language" ).addParameter( "lid" ,
+				java.sql.Types.VARCHAR ).addParameter( "lname" , java.sql.Types.VARCHAR );
 
-		this.uocTranslation = new SimpleJdbcCall( dSource ).withCatalogName( "defs" ).withProcedureName(
-				"uoc_translation" );
-		this.uocTranslation.withoutProcedureColumnMetaDataAccess( );
-		this.uocTranslation.declareParameters( new SqlParameter( "lid" , java.sql.Types.VARCHAR ) , new SqlParameter(
-				"sid" , java.sql.Types.VARCHAR ) , new SqlParameter( "trans" , java.sql.Types.VARCHAR ) );
+		this.uocTranslation = new StoredProc( dSource , "defs" , "uoc_translation" )
+				.addParameter( "lid" , java.sql.Types.VARCHAR ).addParameter( "sid" , java.sql.Types.VARCHAR )
+				.addParameter( "trans" , java.sql.Types.VARCHAR );
 
 		this.tTemplate = new TransactionTemplate( tManager );
 	}
 
 
-	private void importText( TextData data )
+	/**
+	 * Import all languages and strings
+	 * 
+	 * <p>
+	 * Import all language and string definitions from the top-level I18N data instance.
+	 * 
+	 * @param data
+	 *            the top-level I18N data instance
+	 * 
+	 * @throws DataImportException
+	 *             when some external string definition fails to load
+	 */
+	private void importText( I18NText data )
+			throws DataImportException
 	{
-		for ( LanguageData ld : data.languages ) {
+		for ( LanguageDefinition ld : data ) {
 			this.importLanguage( ld );
 		}
 	}
 
 
-	private void importLanguage( LanguageData ld )
+	/**
+	 * Import a language definition and the translations it contains
+	 * 
+	 * <p>
+	 * Import the language's definition, then iterate over all string definitions and import them as
+	 * well.
+	 * 
+	 * @param ld
+	 *            the language's definition data
+	 * 
+	 * @throws DataImportException
+	 *             when some external string definition fails to load
+	 */
+	private void importLanguage( LanguageDefinition ld )
+			throws DataImportException
 	{
-		if ( ld.strings == null ) {
-			return;
-		}
-
 		// Try creating or updating the language
-		this.uocLanguage.execute( ld.id , ld.name );
+		this.uocLanguage.execute( ld.getId( ) , ld.getName( ) );
 
 		// Import translations
-		for ( StringData sd : ld.strings ) {
-			this.uocTranslation.execute( ld.id , sd.id , sd.getString( ) );
+		for ( StringDefinition sd : ld ) {
+			this.uocTranslation.execute( ld.getId( ) , sd.getId( ) , sd.getString( ) );
 		}
 	}
 
 
+	/**
+	 * Run the I18N definitions import tool
+	 * 
+	 * <p>
+	 * Load the data file, then connects to the database and create or update all definitions.
+	 */
 	@Override
 	public void run( )
 	{
-		final TextData data = this.loadData( );
-		if ( data == null ) {
-			System.err.println( "could not read data" );
+		I18NText data;
+		try {
+			data = new I18NLoader( this.file ).load( );
+		} catch ( DataImportException e ) {
+			System.err.println( "Error while loading '" + this.file + "': " + e.getMessage( ) );
+			this.logger.error( "Error while loading I18N data" , e );
 			return;
 		}
 
 		AbstractApplicationContext ctx = this.createContext( );
 		this.createTemplates( ctx );
+		this.executeImportTransaction( data );
+		ToolBase.destroyContext( ctx );
+	}
+
+
+	/**
+	 * Execute the I18N definitions importation transaction
+	 * 
+	 * <p>
+	 * Run a transaction and execute the importation code inside it. Roll back if anything goes
+	 * awry.
+	 * 
+	 * @param data
+	 *            the I18N definitions instance
+	 */
+	private void executeImportTransaction( final I18NText data )
+	{
 		boolean rv = this.tTemplate.execute( new TransactionCallback< Boolean >( ) {
 
 			@Override
 			public Boolean doInTransaction( TransactionStatus status )
 			{
-				boolean rv;
-				try {
-					importText( data );
-					rv = true;
-				} catch ( RuntimeException e ) {
-					logger.error( "Caught runtime exception" , e );
-					rv = false;
-				}
+				boolean rv = ImportText.this.doTransaction( data );
 				if ( !rv ) {
 					status.setRollbackOnly( );
 				}
@@ -243,15 +197,43 @@ public class ImportText
 			}
 
 		} );
-
 		if ( rv ) {
 			this.logger.info( "Text import successful" );
 		}
-
-		ToolBase.destroyContext( ctx );
 	}
 
 
+	/**
+	 * Import transaction body
+	 * 
+	 * <p>
+	 * Import all definitions and handle exceptions.
+	 * 
+	 * @param data
+	 *            the I18N definitions instance
+	 * 
+	 * @return <code>true</code> on success, <code>false</code> otherwise
+	 */
+	private boolean doTransaction( I18NText data )
+	{
+		try {
+			ImportText.this.importText( data );
+			return true;
+		} catch ( RuntimeException e ) {
+			ImportText.this.logger.error( "Caught runtime exception" , e );
+		} catch ( DataImportException e ) {
+			ImportText.this.logger.error( "Error while importing external strings" , e );
+		}
+		return false;
+	}
+
+
+	/**
+	 * Obtain the name of the definitions file
+	 * 
+	 * <p>
+	 * Check the command line options, setting the definitions file accordingly.
+	 */
 	@Override
 	public boolean setOptions( String... options )
 	{
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java
new file mode 100644
index 0000000..2932549
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/I18NLoader.java
@@ -0,0 +1,121 @@
+package com.deepclone.lw.cli.xmlimport;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.i18n.FileString;
+import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText;
+import com.deepclone.lw.cli.xmlimport.data.i18n.InlineString;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.XStreamException;
+
+
+
+/**
+ * I18N text definitions loader
+ * 
+ * <p>
+ * This class can be used to load all I18N definitions. It will extract them from the XML file,
+ * verify them and set their origin.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class I18NLoader
+{
+	/** The file to read the XML from */
+	private final File file;
+
+
+	/**
+	 * Initialise the loader
+	 * 
+	 * @param file
+	 *            the XML file that contains the definitions
+	 */
+	public I18NLoader( File file )
+	{
+		this.file = file.getAbsoluteFile( );
+	}
+
+
+	/**
+	 * Initialise the necessary XStream instance
+	 * 
+	 * <p>
+	 * Initialise the XStream instance by processing annotations in all I18N importable data
+	 * classes.
+	 * 
+	 * @return the XStream instance to use when loading the data
+	 */
+	private XStream initXStream( )
+	{
+		XStream xstream = new XStream( );
+		xstream.processAnnotations( I18NText.class );
+		xstream.processAnnotations( InlineString.class );
+		xstream.processAnnotations( FileString.class );
+		return xstream;
+	}
+
+
+	/**
+	 * Load the I18N definitions
+	 * 
+	 * <p>
+	 * Load the XML file and process the definitions file using XStream.
+	 * 
+	 * @return the top-level importable data instance
+	 * 
+	 * @throws DataImportException
+	 *             if reading from the file or parsing its contents fail
+	 */
+	private I18NText loadXMLFile( )
+			throws DataImportException
+	{
+		FileInputStream fis;
+		try {
+			fis = new FileInputStream( this.file );
+		} catch ( FileNotFoundException e ) {
+			throw new DataImportException( "Unable to load I18N definitions" , e );
+		}
+
+		try {
+			try {
+				XStream xstream = this.initXStream( );
+				return (I18NText) xstream.fromXML( fis );
+			} finally {
+				fis.close( );
+			}
+		} catch ( IOException e ) {
+			throw new DataImportException( "Input error while loading I18N definitions" , e );
+		} catch ( XStreamException e ) {
+			throw new DataImportException( "XML error while loading I18N definitions" , e );
+		}
+	}
+
+
+	/**
+	 * Load and process I18N definition
+	 * 
+	 * <p>
+	 * Attempt to load all I18N definitions, make sure they are valid, then set the original file's
+	 * path.
+	 * 
+	 * @return the top-level importable data instance
+	 * 
+	 * @throws DataImportException
+	 *             if loading or verifying the data fails
+	 */
+	public I18NText load( )
+			throws DataImportException
+	{
+		I18NText text = this.loadXMLFile( );
+		text.verifyData( );
+		text.setReadFrom( this.file );
+		return text;
+	}
+
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java
new file mode 100644
index 0000000..7c70c71
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/DataImportException.java
@@ -0,0 +1,43 @@
+package com.deepclone.lw.cli.xmlimport.data;
+
+
+/**
+ * Data import exception
+ * 
+ * <p>
+ * This exception is thrown by importable data classes when some error occurs while loading, or when
+ * verification fails.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+public class DataImportException
+		extends Exception
+{
+
+	/**
+	 * Initialise a data import exception using an error message
+	 * 
+	 * @param message
+	 *            the error message
+	 */
+	public DataImportException( String message )
+	{
+		super( message );
+	}
+
+
+	/**
+	 * Initialise a data import exception using an message and a cause
+	 * 
+	 * @param message
+	 *            the error message
+	 * @param cause
+	 *            the exception that caused this exception to be thrown
+	 */
+	public DataImportException( String message , Throwable cause )
+	{
+		super( message , cause );
+	}
+
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java
new file mode 100644
index 0000000..fae56bd
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/ImportableData.java
@@ -0,0 +1,66 @@
+package com.deepclone.lw.cli.xmlimport.data;
+
+
+import java.io.File;
+import java.io.Serializable;
+
+
+
+/**
+ * Base class for importable data
+ * 
+ * <p>
+ * This abstract class serves as the base for all classes that represent data imported from XML
+ * files during the database's initialisation.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+public abstract class ImportableData
+		implements Serializable
+{
+
+	/** The file these data were read from */
+	private transient File readFrom;
+
+
+	/** @return the file these data were read from */
+	public final File getReadFrom( )
+	{
+		return this.readFrom;
+	}
+
+
+	/**
+	 * Validate the item
+	 * 
+	 * <p>
+	 * This method may be overridden to implement some form of validation of the loaded data.
+	 * 
+	 * @throws DataImportException
+	 *             if data validation failed
+	 */
+	public void verifyData( )
+			throws DataImportException
+	{
+		// EMPTY
+	}
+
+
+	/**
+	 * Set the source file
+	 * 
+	 * <p>
+	 * This method sets the file instance returned by {@link #getReadFrom()}. For classes that
+	 * represent collections of data, this method should be overridden in order to update all
+	 * contained items.
+	 * 
+	 * @param readFrom
+	 *            the file the data were read from
+	 */
+	public void setReadFrom( File readFrom )
+	{
+		this.readFrom = readFrom;
+	}
+
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java
new file mode 100644
index 0000000..4645170
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/FileString.java
@@ -0,0 +1,104 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+
+
+/**
+ * An external string definition
+ * 
+ * <p>
+ * This class corresponds to I18N string definitions which use external files to store the actual
+ * string.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+@XStreamAlias( "from-file" )
+public class FileString
+		extends StringDefinition
+{
+	/** The name of the file which contains the string's text */
+	@XStreamAsAttribute
+	private String source;
+
+
+	/**
+	 * Verify an external string definition
+	 * 
+	 * <p>
+	 * Make sure that the definition actually includes the name of the file to load the string from.
+	 */
+	@Override
+	public void verifyData( )
+			throws DataImportException
+	{
+		super.verifyData( );
+		if ( this.source == null || "".equals( this.source.trim( ) ) ) {
+			throw new DataImportException( "Missing file name for string definition '" + this.getId( ) + "'" );
+		}
+	}
+
+
+	/**
+	 * Load the string
+	 * 
+	 * <p>
+	 * This implementation opens the file defined as the source of the string's text, reads it then
+	 * returns it.
+	 * 
+	 * @throws DataImportException
+	 *             if the file cannot be opened or read from
+	 */
+	@Override
+	public String getString( )
+			throws DataImportException
+	{
+		File source = getSourceFile( );
+
+		StringBuilder sBuilder = new StringBuilder( );
+		try {
+			BufferedReader in = new BufferedReader( new FileReader( source ) );
+			try {
+				String str;
+				while ( ( str = in.readLine( ) ) != null ) {
+					sBuilder.append( str );
+					sBuilder.append( "\n" );
+				}
+			} finally {
+				in.close( );
+			}
+		} catch ( IOException e ) {
+			throw new DataImportException( "Could not read from " + source.getPath( ) , e );
+		}
+
+		return sBuilder.toString( );
+	}
+
+
+	/**
+	 * Access the source file
+	 * 
+	 * <p>
+	 * Create the source file instance using the path to the data file the current instance was read
+	 * from as the base directory.
+	 * 
+	 * @return the source file
+	 */
+	private File getSourceFile( )
+	{
+		File parentDirectory = this.getReadFrom( ).getParentFile( );
+		if ( parentDirectory == null ) {
+			parentDirectory = new File( "." );
+		}
+		return new File( parentDirectory , this.source ).getAbsoluteFile( );
+	}
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java
new file mode 100644
index 0000000..13cf4da
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/I18NText.java
@@ -0,0 +1,120 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+
+
+/**
+ * I18N text data
+ * 
+ * <p>
+ * This class represents the contents of the I18N text definitions file. It contains a list of
+ * language definitions.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+@XStreamAlias( "lw-text-data" )
+public class I18NText
+		extends ImportableData
+		implements Serializable , Iterable< LanguageDefinition >
+{
+	/** All present language definitions */
+	@XStreamImplicit( itemFieldName = "language" )
+	private List< LanguageDefinition > languages = new LinkedList< LanguageDefinition >( );
+
+
+	/**
+	 * Set the source file
+	 * 
+	 * <p>
+	 * Update the definition's own source file, then update all language definitions.
+	 */
+	@Override
+	public void setReadFrom( File readFrom )
+	{
+		super.setReadFrom( readFrom );
+		for ( LanguageDefinition languageDefinition : this.languages ) {
+			languageDefinition.setReadFrom( readFrom );
+		}
+	}
+
+
+	/**
+	 * Verify I18N text data
+	 * 
+	 * <p>
+	 * Check each definition, then make sure both identifiers and language names are unique.
+	 */
+	@Override
+	public void verifyData( )
+			throws DataImportException
+	{
+		if ( this.languages == null || this.languages.isEmpty( ) ) {
+			throw new DataImportException( "No language definitions" );
+		}
+
+		HashSet< String > identifiers = new HashSet< String >( );
+		HashSet< String > names = new HashSet< String >( );
+		for ( LanguageDefinition definition : this.languages ) {
+			definition.verifyData( );
+			this.checkUniqueItem( "identifier" , identifiers , definition.getId( ) );
+			this.checkUniqueItem( "name" , names , definition.getName( ) );
+		}
+	}
+
+
+	/**
+	 * Check that some part of the definition is unique
+	 * 
+	 * <p>
+	 * This helper method is used by {@link #verifyData()} to make sure that names and identifiers
+	 * are unique.
+	 * 
+	 * @param exceptionText
+	 *            the name of the item
+	 * @param existing
+	 *            the set of existing items
+	 * @param value
+	 *            the item's value
+	 * 
+	 * @throws DataImportException
+	 *             if the item's value is already present in the set of existing items
+	 */
+	private void checkUniqueItem( String exceptionText , HashSet< String > existing , String value )
+			throws DataImportException
+	{
+		if ( existing.contains( value ) ) {
+			throw new DataImportException( "Duplicate language " + exceptionText + " '" + value + "'" );
+		}
+		existing.add( value );
+	}
+
+
+	/**
+	 * Language definition iterator
+	 * 
+	 * <p>
+	 * Grant access to the list of languages in read-only mode.
+	 * 
+	 * @return a read-only iterator on the list of languages.
+	 */
+	@Override
+	public Iterator< LanguageDefinition > iterator( )
+	{
+		return Collections.unmodifiableList( this.languages ).iterator( );
+	}
+
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java
new file mode 100644
index 0000000..01e497e
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/InlineString.java
@@ -0,0 +1,55 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+
+
+/**
+ * An in-line string definition
+ * 
+ * <p>
+ * This class corresponds to string definitions which are stored directly inside the XML data file.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+@XStreamAlias( "inline-string" )
+public class InlineString
+		extends StringDefinition
+{
+
+	/** The string's text */
+	private String value;
+
+
+	/**
+	 * Verify the in-line string definition
+	 * 
+	 * <p>
+	 * Make sure that the definition actually contains a string.
+	 */
+	@Override
+	public void verifyData( )
+			throws DataImportException
+	{
+		super.verifyData( );
+
+		if ( this.value == null || "".equals( this.value.trim( ) ) ) {
+			throw new DataImportException( "Missing or empty in-line string definition '" + this.getId( ) + "'" );
+		}
+	}
+
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see StringDefinition#getString()
+	 */
+	@Override
+	public String getString( )
+	{
+		return this.value;
+	}
+}
\ No newline at end of file
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java
new file mode 100644
index 0000000..77b17d1
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/LanguageDefinition.java
@@ -0,0 +1,145 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+import com.thoughtworks.xstream.annotations.XStreamImplicit;
+
+
+
+/**
+ * A language definition
+ * 
+ * <p>
+ * Language definitions for the I18N text importer possess an identifier and a name. In addition,
+ * they may contain any amount of string definitions.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+public class LanguageDefinition
+		extends ImportableData
+		implements Serializable , Iterable< StringDefinition >
+{
+	/** The language's identifier */
+	@XStreamAsAttribute
+	private String id;
+
+	/** The language's name */
+	@XStreamAsAttribute
+	private String name;
+
+	/** All strings defined for this language */
+	@XStreamImplicit
+	private final List< StringDefinition > strings = new LinkedList< StringDefinition >( );
+
+
+	/**
+	 * Set the source file
+	 * 
+	 * <p>
+	 * Update the definition's own source file, then update all string definitions.
+	 */
+	@Override
+	public void setReadFrom( File readFrom )
+	{
+		super.setReadFrom( readFrom );
+		for ( StringDefinition definition : this.strings ) {
+			definition.setReadFrom( readFrom );
+		}
+	}
+
+
+	/**
+	 * Validate the language definition
+	 * 
+	 * <p>
+	 * Make sure that all definition characteristics are properly defined, then check all strings
+	 * and make sure they are unique.
+	 */
+	@Override
+	public void verifyData( )
+			throws DataImportException
+	{
+		if ( this.id == null || "".equals( this.id.trim( ) ) ) {
+			throw new DataImportException( "Missing language identifier" );
+		}
+		if ( this.name == null || "".equals( this.name.trim( ) ) ) {
+			throw new DataImportException( "Missing language name for identifier '" + this.id + "'" );
+		}
+		if ( this.strings == null || this.strings.isEmpty( ) ) {
+			throw new DataImportException( "No strings defined for language '" + this.id + "'" );
+		}
+
+		this.checkStringDefinitions( );
+	}
+
+
+	/**
+	 * Check all string definitions
+	 * 
+	 * <p>
+	 * Iterate over all included string definitions, validating them and making sure they all have
+	 * unique identifiers.
+	 * 
+	 * @throws DataImportException
+	 *             if a string definition is invalid or if string identifiers contain duplicates.
+	 */
+	private void checkStringDefinitions( )
+			throws DataImportException
+	{
+		HashSet< String > strings = new HashSet< String >( );
+		for ( StringDefinition definition : this.strings ) {
+			definition.verifyData( );
+
+			String stringId = definition.getId( );
+			if ( strings.contains( stringId ) ) {
+				throw new DataImportException( "In language '" + this.id + "': duplicate string '" + stringId + "'" );
+			}
+			strings.add( stringId );
+		}
+	}
+
+
+	/** @return the language's identifier */
+	public String getId( )
+	{
+		return this.id;
+	}
+
+
+	/** @return the language's name */
+	public String getName( )
+	{
+		return this.name;
+	}
+
+
+	/**
+	 * Iterate over all string definitions
+	 * 
+	 * <p>
+	 * This method grants access to the list of string definitions in read-only mode.
+	 * 
+	 * <p>
+	 * <strong>Warning:</strong> this method should not be called if {@link #containsStrings()}
+	 * returns <code>false</code>.
+	 * 
+	 * @return the read-only string definition iterator
+	 */
+	@Override
+	public Iterator< StringDefinition > iterator( )
+	{
+		return Collections.unmodifiableList( this.strings ).iterator( );
+	}
+
+}
diff --git a/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java
new file mode 100644
index 0000000..1e2701a
--- /dev/null
+++ b/legacyworlds-server-main/src/main/java/com/deepclone/lw/cli/xmlimport/data/i18n/StringDefinition.java
@@ -0,0 +1,73 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.Serializable;
+import java.util.regex.Pattern;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+
+
+/**
+ * A string definition
+ * 
+ * <p>
+ * This class is the base for both types of string definitions used by the initial I18N text
+ * importer. String definitions always contain an identifier, and it is always possible to obtain
+ * the actual text they contain.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+@SuppressWarnings( "serial" )
+public abstract class StringDefinition
+		extends ImportableData
+		implements Serializable
+{
+	/** Pattern followed by string identifiers */
+	private static final Pattern identifierCheck = Pattern.compile( "^[a-z][a-z0-9]*([A-Z][a-z0-9]*)*$" );
+
+	/** The string's identifier */
+	@XStreamAsAttribute
+	private String id;
+
+
+	/**
+	 * Check a string definition's identifier
+	 * 
+	 * <p>
+	 * Make sure that a string definition's identifier is both present and valid.
+	 */
+	@Override
+	public void verifyData( )
+			throws DataImportException
+	{
+		if ( this.id == null ) {
+			throw new DataImportException( "Missing string identifier" );
+		} else if ( !identifierCheck.matcher( this.id ).find( ) ) {
+			throw new DataImportException( "Invalid string identifier '" + this.id + "'" );
+		}
+	}
+
+
+	/** @return the string's identifier */
+	public final String getId( )
+	{
+		return this.id;
+	}
+
+
+	/**
+	 * This method must be overridden to provide a way of reading the actual text of the string that
+	 * is being defined.
+	 * 
+	 * @return the string's text
+	 * 
+	 * @throws DataImportException
+	 *             if accessing the string's actual text fails
+	 */
+	public abstract String getString( )
+			throws DataImportException;
+
+}
diff --git a/legacyworlds-server-main/src/main/resources/log4j.properties b/legacyworlds-server-main/src/main/resources/log4j.properties
index 994c335..066fac3 100644
--- a/legacyworlds-server-main/src/main/resources/log4j.properties
+++ b/legacyworlds-server-main/src/main/resources/log4j.properties
@@ -36,3 +36,4 @@ log4j.logger.org.springframework=WARN, server
 log4j.logger.org.springframework=INFO, fullDebug
 log4j.logger.com.deepclone.lw=DEBUG, fullDebug
 log4j.logger.com.deepclone.lw.interfaces.eventlog=DEBUG, server
+log4j.logger.com.deepclone.lw.cli=INFO, stdout
diff --git a/legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt b/legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt
new file mode 100644
index 0000000..273c1a9
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-external-file/test.txt
@@ -0,0 +1 @@
+This is a test.
\ No newline at end of file
diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml
new file mode 100644
index 0000000..f33b821
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-contents.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lw-text-data>
+
+	<does-not-exist />
+
+</lw-text-data>
\ No newline at end of file
diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml
new file mode 100644
index 0000000..aec5ebc
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-data.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text ../../../legacyworlds-server-main/data/i18n-text.xsd">
+
+	<language name="test" id="test">
+		<inline-string id="test">
+			<value />
+		</inline-string>
+	</language>
+
+</lw-text-data>
\ No newline at end of file
diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml
new file mode 100644
index 0000000..ee1d0ed
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-loader/bad-xml.xml
@@ -0,0 +1,2 @@
+This is not an XML file, obviously.
+We'll make that even more confusing: <<<<<< & >>!!!
\ No newline at end of file
diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/good.xml b/legacyworlds-server-tests/TestFiles/i18n-loader/good.xml
new file mode 100644
index 0000000..f1ca627
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-loader/good.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lw-text-data xmlns="http://www.deepclone.com/lw/b6/m1/i18n-text"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.deepclone.com/lw/b6/m1/i18n-text ../../../legacyworlds-server-main/data/i18n-text.xsd">
+
+	<language name="test" id="test">
+	
+		<from-file id="test" source="test.txt" />
+
+	</language>
+
+</lw-text-data>
\ No newline at end of file
diff --git a/legacyworlds-server-tests/TestFiles/i18n-loader/test.txt b/legacyworlds-server-tests/TestFiles/i18n-loader/test.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/legacyworlds-server-tests/TestFiles/i18n-loader/test.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/legacyworlds-server-tests/pom.xml b/legacyworlds-server-tests/pom.xml
index d1b1438..c9cd3a4 100644
--- a/legacyworlds-server-tests/pom.xml
+++ b/legacyworlds-server-tests/pom.xml
@@ -50,6 +50,10 @@
 			<artifactId>legacyworlds-server-beans-user</artifactId>
 			<groupId>com.deepclone.lw</groupId>
 		</dependency>
+		<dependency>
+			<artifactId>legacyworlds-server-main</artifactId>
+			<groupId>com.deepclone.lw</groupId>
+		</dependency>
 
 		<dependency>
 			<groupId>junit</groupId>
diff --git a/legacyworlds-server-tests/src/main/java/.empty b/legacyworlds-server-tests/src/main/java/.empty
new file mode 100644
index 0000000..e69de29
diff --git a/legacyworlds-server-tests/src/main/resources/.empty b/legacyworlds-server-tests/src/main/resources/.empty
new file mode 100644
index 0000000..e69de29
diff --git a/legacyworlds-server-tests/src/test/java/.empty b/legacyworlds-server-tests/src/test/java/.empty
new file mode 100644
index 0000000..e69de29
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java
new file mode 100644
index 0000000..0b2ac67
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/TestI18NLoader.java
@@ -0,0 +1,152 @@
+package com.deepclone.lw.cli.xmlimport;
+
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.i18n.I18NText;
+import com.deepclone.lw.cli.xmlimport.data.i18n.LanguageDefinition;
+import com.deepclone.lw.cli.xmlimport.data.i18n.StringDefinition;
+import com.thoughtworks.xstream.converters.ConversionException;
+import com.thoughtworks.xstream.io.StreamException;
+
+
+
+/**
+ * Unit tests for the {@link I18NLoader} class.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ * 
+ */
+public class TestI18NLoader
+{
+
+	/**
+	 * Try initialising the loader with a <code>null</code> file instance.
+	 * 
+	 * @throws NullPointerException
+	 *             when the constructor is executed
+	 */
+	@Test( expected = NullPointerException.class )
+	public void testNullFile( )
+			throws NullPointerException
+	{
+		new I18NLoader( null );
+	}
+
+
+	/**
+	 * Try loading a file that does not exist
+	 */
+	@Test
+	public void testMissingFile( )
+	{
+		I18NLoader loader = new I18NLoader( new File( "does-not-exist" ) );
+		try {
+			loader.load( );
+		} catch ( DataImportException e ) {
+			assertTrue( "cause is a " + e.getCause( ).getClass( ).getName( ) , e.getCause( ) instanceof IOException );
+			return;
+		}
+		fail( "no exception after trying to load a missing file" );
+	}
+
+
+	/**
+	 * Try loading a file that contains something that definitely isn't XML.
+	 */
+	@Test
+	public void testBadXML( )
+	{
+		I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-loader/bad-xml.xml" ) );
+		try {
+			loader.load( );
+		} catch ( DataImportException e ) {
+			assertTrue( "cause is a " + e.getCause( ).getClass( ).getName( ) , e.getCause( ) instanceof StreamException );
+			return;
+		}
+		fail( "no exception after loading stuff that isn't XML" );
+	}
+
+
+	/**
+	 * Try loading a file that contains XML but which cannot be desertialised to an {@link I18NText}
+	 * instance.
+	 */
+	@Test
+	public void testBadContents( )
+	{
+		I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-loader/bad-contents.xml" ) );
+		try {
+			loader.load( );
+		} catch ( DataImportException e ) {
+			assertTrue( "cause is a " + e.getCause( ).getClass( ).getName( ) ,
+					e.getCause( ) instanceof ConversionException );
+			return;
+		}
+		fail( "no exception after loading bad XML" );
+	}
+
+
+	/**
+	 * Try loading a file that contains valid XML for an {@link I18NText} instance with semantic
+	 * errors.
+	 */
+	@Test
+	public void testBadData( )
+	{
+		I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-loader/bad-data.xml" ) );
+		try {
+			loader.load( );
+		} catch ( DataImportException e ) {
+			assertNull( e.getCause( ) );
+			return;
+		}
+		fail( "no exception after loading bad data" );
+	}
+
+
+	/**
+	 * Try loading valid data, make sure that it contains exactly what is expected, and that it
+	 * loads external strings from the appropriate directory.
+	 */
+	@Test
+	public void testGoodData( )
+	{
+		I18NLoader loader = new I18NLoader( new File( "TestFiles/i18n-loader/good.xml" ) );
+		I18NText text;
+		try {
+			text = loader.load( );
+		} catch ( DataImportException e ) {
+			fail( "could not load valid file" );
+			return;
+		}
+		assertNotNull( text );
+
+		int lCount = 0;
+		for ( LanguageDefinition ld : text ) {
+			assertEquals( "test" , ld.getId( ) );
+			assertEquals( "test" , ld.getName( ) );
+
+			int tCount = 0;
+			for ( StringDefinition sd : ld ) {
+				assertEquals( "test" , sd.getId( ) );
+				try {
+					assertEquals( "test" , sd.getString( ).trim( ) );
+				} catch ( DataImportException e ) {
+					fail( "could not load string: " + e.getMessage( ) );
+				}
+				tCount++;
+			}
+
+			assertEquals( 1 , tCount );
+			lCount++;
+		}
+		assertEquals( 1 , lCount );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java
new file mode 100644
index 0000000..f8f5880
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/TestImportableData.java
@@ -0,0 +1,73 @@
+package com.deepclone.lw.cli.xmlimport.data;
+
+
+import java.io.File;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+
+/**
+ * Unit tests for the {@link ImportableData} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestImportableData
+{
+
+	/**
+	 * Dummy importable data
+	 * 
+	 * <p>
+	 * This class is used in the test to have access to an importable data instance that corresponds
+	 * exactly to the base class.
+	 * 
+	 * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+	 */
+	@SuppressWarnings( "serial" )
+	private static class DummyData
+			extends ImportableData
+	{
+		// EMPTY
+	}
+
+
+	/** Make sure that {@link ImportableData#getReadFrom()} returns <code>null</code> by default */
+	@Test
+	public void testUninitialisedReadFrom( )
+	{
+		DummyData data = new DummyData( );
+		assertNull( data.getReadFrom( ) );
+	}
+
+
+	/** Checks the default implementation of {@link ImportableData#setReadFrom(File)} */
+	@Test
+	public void testSetReadFrom( )
+	{
+		DummyData data = new DummyData( );
+		File file = new File( "test" );
+
+		data.setReadFrom( file );
+		assertNotNull( data.getReadFrom( ) );
+		assertEquals( "test" , data.getReadFrom( ).getPath( ) );
+	}
+
+
+	/**
+	 * Make sure that the default implementation of {@link ImportableData#verifyData()} always
+	 * succeeds
+	 * 
+	 * @throws DataImportException
+	 *             if the default implementation fails somehow
+	 */
+	@Test
+	public void testDefaultVerify( )
+			throws DataImportException
+	{
+		DummyData data = new DummyData( );
+		data.verifyData( );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java
new file mode 100644
index 0000000..1daeca8
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/BaseTest.java
@@ -0,0 +1,205 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import com.deepclone.lw.cli.xmlimport.I18NLoader;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.XStreamException;
+
+
+
+/**
+ * Base class for I18N import structures
+ * 
+ * <p>
+ * This class is used as a parent by tests of the I18N import structures. It includes the code
+ * required to actually create such resources, as this can normally only be done through XStream.
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ * 
+ */
+abstract class BaseTest
+{
+
+	/**
+	 * Escape &amp;, &lt; and &gt; in XML strings
+	 * 
+	 * @param string
+	 *            the string to escape
+	 * 
+	 * @return the escaped string
+	 */
+	private String xmlString( String string )
+	{
+		return string.replace( "&" , "&amp;" ).replace( "<" , "&lt;" ).replace( ">" , "&gt;" );
+	}
+
+
+	/**
+	 * Escape &amp;, &lt; and &gt;, ' and " in XML strings
+	 * 
+	 * @param string
+	 *            the string to escape
+	 * 
+	 * @return the escaped string
+	 */
+	private String quoteString( String string )
+	{
+		return "\"" + this.xmlString( string ).replace( "\"" , "&quot;" ).replace( "'" , "&apos;" ) + "\"";
+	}
+
+
+	/**
+	 * Create an in-line string definition
+	 * 
+	 * <p>
+	 * Generate the XML code that corresponds to an in-line string definition.
+	 * 
+	 * @param identifier
+	 *            the string's identifier, or <code>null</code> to omit the identifier
+	 * @param contents
+	 *            the string's contents, or <code>null</code> to omit the contents.
+	 * 
+	 * @return the XML code corresponding to the string definition
+	 */
+	protected String createInlineStringDefinition( String identifier , String contents )
+	{
+		StringBuilder str = new StringBuilder( );
+		str.append( "<inline-string" );
+		if ( identifier != null ) {
+			str.append( " id=" ).append( this.quoteString( identifier ) );
+		}
+		str.append( '>' );
+		if ( contents != null ) {
+			str.append( "<value>" ).append( this.xmlString( contents ) ).append( "</value>" );
+		}
+		str.append( "</inline-string>" );
+		return str.toString( );
+	}
+
+
+	/**
+	 * Create an external string definition
+	 * 
+	 * <p>
+	 * Generate the XML code that corresponds to an external string definition.
+	 * 
+	 * @param identifier
+	 *            the string's identifier, or <code>null</code> to omit the identifier
+	 * @param source
+	 *            the string's source file, or <code>null</code> to omit the source.
+	 * 
+	 * @return the XML code corresponding to the string definition
+	 */
+	protected String createExternalStringDefinition( String identifier , String source )
+	{
+		StringBuilder str = new StringBuilder( );
+		str.append( "<from-file" );
+		if ( identifier != null ) {
+			str.append( " id=" ).append( this.quoteString( identifier ) );
+		}
+		if ( source != null ) {
+			str.append( " source=" ).append( this.quoteString( source ) );
+		}
+		str.append( " />" );
+		return str.toString( );
+	}
+
+
+	/**
+	 * Create a language definition
+	 * 
+	 * @param identifier
+	 *            the language's identifier, or <code>null</code> if the identifier is to be omitted
+	 * @param name
+	 *            the language's name, or <code>null</code> if the name is to be omitted
+	 * @param stringDefinitions
+	 *            XML definitions returned by {@link #createInlineStringDefinition(String, String)}
+	 *            or {@link #createExternalStringDefinition(String, String)}
+	 * 
+	 * @return the XML code corresponding to the language definition
+	 */
+	protected String createLanguageDefinition( String identifier , String name , String... stringDefinitions )
+	{
+		StringBuilder str = new StringBuilder( );
+
+		str.append( "<language" );
+		if ( identifier != null ) {
+			str.append( " id=" ).append( this.quoteString( identifier ) );
+		}
+		if ( name != null ) {
+			str.append( " name=" ).append( this.quoteString( name ) );
+		}
+		str.append( ">" );
+		for ( String definition : stringDefinitions ) {
+			str.append( definition );
+		}
+		str.append( "</language>" );
+
+		return str.toString( );
+	}
+
+
+	/**
+	 * Create the XML code for a top-level I18N definition element
+	 * 
+	 * @param languages
+	 *            XML definitions of languages, as returned by
+	 *            {@link #createLanguageDefinition(String, String, String...)}
+	 * 
+	 * @return the top-level element's XML code
+	 */
+	protected String createTopLevel( String... languages )
+	{
+		StringBuilder str = new StringBuilder( );
+		str.append( "<lw-text-data>" );
+		for ( String language : languages ) {
+			str.append( language );
+		}
+		str.append( "</lw-text-data>" );
+		return str.toString( );
+	}
+
+
+	/**
+	 * Create the necessary XStream instance
+	 * 
+	 * <p>
+	 * Initialise an XStream instance and set it up so it can process I18N data definitions. Unlike
+	 * the XStream instance generated by {@link I18NLoader}, this version also registers the alias
+	 * for language definitions.
+	 * 
+	 * @return the initialised XStream instance.
+	 */
+	private XStream createXStreamInstance( )
+	{
+		XStream xstream = new XStream( );
+		xstream.processAnnotations( I18NText.class );
+		xstream.processAnnotations( InlineString.class );
+		xstream.processAnnotations( FileString.class );
+		xstream.alias( "language" , LanguageDefinition.class );
+		return xstream;
+	}
+
+
+	/**
+	 * Create an I18N definition object from its XML code
+	 * 
+	 * @param xml
+	 *            the XML code
+	 * @param cls
+	 *            the class of the object
+	 * 
+	 * @return the object that was created from the code
+	 * 
+	 * @throws ClassCastException
+	 *             if the code corresponds to some other type of object
+	 * @throws XStreamException
+	 *             if some error occurred while unserialising the object
+	 */
+	protected < T extends ImportableData > T createObject( String xml , Class< T > cls )
+			throws ClassCastException , XStreamException
+	{
+		return cls.cast( this.createXStreamInstance( ).fromXML( xml ) );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestExternalString.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestExternalString.java
new file mode 100644
index 0000000..6970672
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestExternalString.java
@@ -0,0 +1,307 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+
+
+
+/**
+ * Unit tests for the {@link FileString} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestExternalString
+		extends BaseTest
+{
+
+	/**
+	 * Test loading a valid external string record and the corresponding contents
+	 * 
+	 * @throws DataImportException
+	 *             if a failure occurs while reading the source file.
+	 */
+	@Test
+	public void testLoadStringOK( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+		assertEquals( "test" , instance.getId( ) );
+		assertEquals( "This is a test." , instance.getString( ).trim( ) );
+	}
+
+
+	/**
+	 * Test loading an external string record with no identifier
+	 * 
+	 * @throws DataImportException
+	 *             if a failure occurs while reading the source file.
+	 */
+	@Test
+	public void testLoadStringNullIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( null , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+		assertNull( instance.getId( ) );
+		assertEquals( "This is a test." , instance.getString( ).trim( ) );
+	}
+
+
+	/**
+	 * Test loading an external string record with no source file
+	 * 
+	 * @throws DataImportException
+	 *             if a failure occurs while reading the source file
+	 * @throws NullPointerException
+	 *             when {@link FileString#getString()} is called while its source is
+	 *             <code>null</code>
+	 */
+	@Test( expected = NullPointerException.class )
+	public void testLoadStringNullSource( )
+			throws DataImportException , NullPointerException
+	{
+		String definition = this.createExternalStringDefinition( "test" , null );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+		assertEquals( "test" , instance.getId( ) );
+		instance.getString( );
+	}
+
+
+	/**
+	 * Test loading a valid external string record and obtaining the contents without setting the
+	 * path to the data file first.
+	 * 
+	 * @throws DataImportException
+	 *             if a failure occurs while reading the source file.
+	 * @throws NullPointerException
+	 *             when {@link FileString#getString()} is called while its original path is
+	 *             <code>null</code>
+	 */
+	@Test( expected = NullPointerException.class )
+	public void testLoadStringNoPath( )
+			throws DataImportException , NullPointerException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.getString( );
+	}
+
+
+	/**
+	 * Test loading a valid external string record and obtaining the contents when the source file
+	 * does not exist.
+	 * 
+	 * @throws DataImportException
+	 *             when a failure occurs while reading the source file.
+	 */
+	@Test( expected = DataImportException.class )
+	public void testLoadStringBadSource( )
+			throws DataImportException , NullPointerException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "does-not-exist.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+		instance.getString( );
+	}
+
+
+	/**
+	 * Test loading a valid external string record and obtaining the contents when the source file
+	 * is a directory.
+	 * 
+	 * @throws DataImportException
+	 *             when a failure occurs while reading the source file.
+	 */
+	@Test( expected = DataImportException.class )
+	public void testLoadStringSourceIsDirectory( )
+			throws DataImportException , NullPointerException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "." );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "TestFiles/i18n-external-file/file" ) );
+		instance.getString( );
+	}
+
+
+	/**
+	 * Test loading a valid external string record and obtaining the contents when the original
+	 * file's path is incorrect.
+	 * 
+	 * @throws DataImportException
+	 *             when a failure occurs while reading the source file.
+	 */
+	@Test( expected = DataImportException.class )
+	public void testLoadStringBadParent( )
+			throws DataImportException , NullPointerException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "does-not-exist/does-not-exist" ) );
+		instance.getString( );
+	}
+
+
+	/**
+	 * Test loading a valid external string record and obtaining the contents when the original
+	 * file's path is a single file name.
+	 * 
+	 * @throws DataImportException
+	 *             if a failure occurs while reading the source file.
+	 */
+	@Test
+	public void testLoadStringParentIsCurrentDirectory( )
+			throws DataImportException , NullPointerException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "TestFiles/i18n-external-file/test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.setReadFrom( new File( "does-not-exist" ) );
+		instance.getString( );
+	}
+
+
+
+	/** Test validating a file string record with a short identifier and a source */
+	@Test
+	public void testValidateSimple( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test validating an external string record with a camel case identifier and a source */
+	@Test
+	public void testValidateCamelCaseIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "testCamelCase" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test validating an external string record with an identifier that includes a sequence of
+	 * upper-case letters and a source
+	 */
+	@Test
+	public void testValidateCapsIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "testCAPSIdentifier" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test validating an external string record with an identifier that includes numbers and some
+	 * contents
+	 */
+	@Test
+	public void testValidateNumbersIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test123Numbers123" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test rejecting an external string record with a <code>null</code> identifier and a source
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateNullIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( null , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an external string record with an identifier that starts with a bad character */
+	@Test( expected = DataImportException.class )
+	public void testValidateBadFirstCharacterIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( " test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test rejecting an external string record with an identifier that starts with an upper-case
+	 * letter
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateFirstCharacterWrongCaseIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "Test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an external string record with an identifier that includes "bad" characters */
+	@Test( expected = DataImportException.class )
+	public void testValidateBadMiddleCharacterIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test Test" , "test.txt" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an external record with <code>null</code> contents */
+	@Test( expected = DataImportException.class )
+	public void testValidateNullSource( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test" , null );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an external record with empty contents */
+	@Test( expected = DataImportException.class )
+	public void testValidateEmptySource( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test" , "" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test rejecting an external record with contents the include only white space (spaces, tabs,
+	 * new line, line feed)
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateSpacesOnlySource( )
+			throws DataImportException
+	{
+		String definition = this.createExternalStringDefinition( "test" , " \t\r\n" );
+		FileString instance = this.createObject( definition , FileString.class );
+		instance.verifyData( );
+	}
+
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java
new file mode 100644
index 0000000..f158d91
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestI18NText.java
@@ -0,0 +1,134 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+
+
+
+/**
+ * Unit tests for the {@link I18NText} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestI18NText
+		extends BaseTest
+{
+	/** The language definition that will be used in most tests */
+	private String languageDefinition;
+
+
+	/** Initialise a language definition to use while testing */
+	@Before
+	public void setUp( )
+	{
+		String xml = this.createInlineStringDefinition( "test" , "test" );
+		this.languageDefinition = this.createLanguageDefinition( "test" , "test" , xml );
+	}
+
+
+	/**
+	 * Try loading a valid top-level I18N definition and make sure its contents are correct
+	 * 
+	 * @throws DataImportException
+	 *             if the language definition check fails for some unknown reason
+	 */
+	@Test
+	public void testLoadValidDefinitions( )
+			throws DataImportException
+	{
+		String xml = this.createTopLevel( this.languageDefinition );
+		I18NText text = this.createObject( xml , I18NText.class );
+
+		int count = 0;
+		for ( LanguageDefinition definition : text ) {
+			definition.verifyData( );
+			count++;
+		}
+		assertEquals( 1 , count );
+	}
+
+
+	/**
+	 * Try loading an empty top-level element
+	 * 
+	 * @throws NullPointerException
+	 *             when the iterator is accessed
+	 */
+	@Test( expected = NullPointerException.class )
+	public void testLoadEmpty( )
+			throws NullPointerException
+	{
+		String xml = this.createTopLevel( );
+		I18NText text = this.createObject( xml , I18NText.class );
+		text.iterator( );
+	}
+
+
+	/**
+	 * Try validating a correct top-level element
+	 * 
+	 * @throws DataImportException
+	 *             if the check fails
+	 */
+	@Test
+	public void testValidateOK( )
+			throws DataImportException
+	{
+		String xml = this.createTopLevel( this.languageDefinition );
+		I18NText text = this.createObject( xml , I18NText.class );
+		text.verifyData( );
+	}
+
+
+	/**
+	 * Try rejecting a top-level element that does not contain any language definition
+	 * 
+	 * @throws DataImportException
+	 *             when the check fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateEmpty( )
+			throws DataImportException
+	{
+		String xml = this.createTopLevel( );
+		I18NText text = this.createObject( xml , I18NText.class );
+		text.verifyData( );
+	}
+
+
+	/**
+	 * Try rejecting a top-level element that contains an invalid language definition
+	 * 
+	 * @throws DataImportException
+	 *             when the check fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateBadDefinition( )
+			throws DataImportException
+	{
+		String xml = this.createTopLevel( this.createLanguageDefinition( null , null ) );
+		I18NText text = this.createObject( xml , I18NText.class );
+		text.verifyData( );
+	}
+
+
+	/**
+	 * Try rejecting a top-level element that contains two valid but duplicate language definitions
+	 * 
+	 * @throws DataImportException
+	 *             when the check fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateDuplicateDefinition( )
+			throws DataImportException
+	{
+		String xml = this.createTopLevel( this.languageDefinition , this.languageDefinition );
+		I18NText text = this.createObject( xml , I18NText.class );
+		text.verifyData( );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java
new file mode 100644
index 0000000..501ce92
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestInlineString.java
@@ -0,0 +1,210 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import java.io.File;
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+import com.deepclone.lw.cli.xmlimport.data.ImportableData;
+
+import static org.junit.Assert.*;
+
+
+
+/**
+ * Tests for the {@link InlineString} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestInlineString
+		extends BaseTest
+{
+
+	/** Test loading a valid in-line string record */
+	@Test
+	public void testLoadStringOK( )
+	{
+		String definition = this.createInlineStringDefinition( "test" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		assertEquals( "test" , instance.getId( ) );
+		assertEquals( "Test" , instance.getString( ) );
+	}
+
+
+	/** Test loading an in-line string record without identifier */
+	@Test
+	public void testLoadStringNoIdentifier( )
+	{
+		String definition = this.createInlineStringDefinition( null , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		assertNull( instance.getId( ) );
+		assertEquals( "Test" , instance.getString( ) );
+	}
+
+
+	/** Test loading an in-line string record without contents */
+	@Test
+	public void testLoadStringNoContents( )
+	{
+		String definition = this.createInlineStringDefinition( "test" , null );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		assertEquals( "test" , instance.getId( ) );
+		assertNull( instance.getString( ) );
+	}
+
+
+	/** Test validating an in-line string record with a short identifier and some contents */
+	@Test
+	public void testValidateSimple( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "test" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test validating an in-line string record with a camel case identifier and some contents */
+	@Test
+	public void testValidateCamelCaseIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "testCamelCase" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test validating an in-line string record with an identifier that includes a sequence of
+	 * upper-case letters and some contents
+	 */
+	@Test
+	public void testValidateCapsIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "testCAPSIdentifier" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test validating an in-line string record with an identifier that includes numbers and some
+	 * contents
+	 */
+	@Test
+	public void testValidateNumbersIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "test123Numbers123" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test rejecting an in-line string record with a <code>null</code> identifier and some contents
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateNullIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( null , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an in-line string record with an identifier that starts with a bad character */
+	@Test( expected = DataImportException.class )
+	public void testValidateBadFirstCharacterIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( " test" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test rejecting an in-line string record with an identifier that starts with an upper-case
+	 * letter
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateFirstCharacterWrongCaseIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "Test" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an in-line string record with an identifier that includes "bad" characters */
+	@Test( expected = DataImportException.class )
+	public void testValidateBadMiddleCharacterIdentifier( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "test Test" , "Test" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an in-line record with <code>null</code> contents */
+	@Test( expected = DataImportException.class )
+	public void testValidateNullContents( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "test" , null );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/** Test rejecting an in-line record with empty contents */
+	@Test( expected = DataImportException.class )
+	public void testValidateEmptyContents( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "test" , "" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Test rejecting an in-line record with contents the include only white space (spaces, tabs,
+	 * new line, line feed)
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateSpacesOnlyContents( )
+			throws DataImportException
+	{
+		String definition = this.createInlineStringDefinition( "test" , " \t\r\n" );
+		InlineString instance = this.createObject( definition , InlineString.class );
+		instance.verifyData( );
+	}
+
+
+	/**
+	 * Make sure the class does not override {@link ImportableData#setReadFrom(java.io.File)}; if it
+	 * does, new tests <strong>must</strong> be written.
+	 * 
+	 * @throws NoSuchMethodException
+	 *             if the method has been deleted
+	 * @throws SecurityException
+	 *             if the JVM set-up is getting in the wayy
+	 */
+	@Test
+	public void testNoSetReadFromOverride( )
+			throws SecurityException , NoSuchMethodException
+	{
+		Class< InlineString > cls = InlineString.class;
+		Method method = cls.getMethod( "setReadFrom" , File.class );
+		assertEquals( ImportableData.class , method.getDeclaringClass( ) );
+	}
+}
diff --git a/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java
new file mode 100644
index 0000000..d9b18ef
--- /dev/null
+++ b/legacyworlds-server-tests/src/test/java/com/deepclone/lw/cli/xmlimport/data/i18n/TestLanguageDefinition.java
@@ -0,0 +1,264 @@
+package com.deepclone.lw.cli.xmlimport.data.i18n;
+
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.deepclone.lw.cli.xmlimport.data.DataImportException;
+
+
+
+/**
+ * Unit tests for the {@link LanguageDefinition} class
+ * 
+ * @author <a href="mailto:tseeker@legacyworlds.com">E. Benoît</a>
+ */
+public class TestLanguageDefinition
+		extends BaseTest
+{
+
+	/** The in-line string definition that will be used in most tests */
+	private String inlineDefinition;
+
+
+	/**
+	 * Create a valid in-line string definition to be appended to language definitions.
+	 */
+	@Before
+	public void setUp( )
+	{
+		this.inlineDefinition = this.createInlineStringDefinition( "test" , "test" );
+	}
+
+
+	/**
+	 * Test loading a valid language definition and make sure that all fields were set
+	 * appropriately.
+	 * 
+	 * @throws DataImportException
+	 *             if accessing the test string fails.
+	 */
+	@Test
+	public void testLoadValidDefinition( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , "Test" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		assertEquals( "t" , language.getId( ) );
+		assertEquals( "Test" , language.getName( ) );
+
+		int count = 0;
+		for ( StringDefinition stringDefinition : language ) {
+			assertEquals( InlineString.class , stringDefinition.getClass( ) );
+			assertEquals( "test" , stringDefinition.getId( ) );
+			assertEquals( "test" , stringDefinition.getString( ) );
+			count++;
+		}
+		assertEquals( 1 , count );
+	}
+
+
+	/**
+	 * Test loading a language definition that has no identifier.
+	 */
+	@Test
+	public void testLoadNullIdentifier( )
+	{
+		String xml = this.createLanguageDefinition( null , "Test" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		assertNull( language.getId( ) );
+	}
+
+
+	/**
+	 * Test loading a language definition that has no name
+	 */
+	@Test
+	public void testLoadNullName( )
+	{
+		String xml = this.createLanguageDefinition( "test" , null , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		assertNull( language.getName( ) );
+	}
+
+
+	/**
+	 * Test loading a language definition that has no contents
+	 * 
+	 * @throws NullPointerException
+	 *             when the language definition's iterator is accessed, as its contents will be
+	 *             <code>null</code>
+	 */
+	@Test( expected = NullPointerException.class )
+	public void testLoadNullContents( )
+			throws NullPointerException
+	{
+		String xml = this.createLanguageDefinition( "test" , "test" );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.iterator( );
+	}
+
+
+	/**
+	 * Test validating a correct language definition.
+	 * 
+	 * @throws DataImportException
+	 *             if validating the definition fails
+	 */
+	@Test
+	public void testValidateOK( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , "Test" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with a <code>null</code> identifier.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateNullIdentifier( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( null , "Test" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with an empty identifier.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateEmptyIdentifier( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "" , "Test" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with an identifier that contains white space only.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateWhiteSpaceIdentifier( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( " \t\n\r" , "Test" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with a <code>null</code> name.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateNullName( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , null , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with an empty name.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateEmptyName( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , "" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with a name that contains white space only.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateWhiteSpaceName( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , " \t\n\r" , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with no contents.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateNoContents( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , "Test" );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with invalid contents.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateBadContents( )
+			throws DataImportException
+	{
+		String strXML = this.createInlineStringDefinition( null , "test" );
+		String xml = this.createLanguageDefinition( "t" , "Test" , strXML );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+
+	/**
+	 * Test validating a language definition with duplicate contents.
+	 * 
+	 * @throws DataImportException
+	 *             when validation fails
+	 */
+	@Test( expected = DataImportException.class )
+	public void testValidateDuplicateContents( )
+			throws DataImportException
+	{
+		String xml = this.createLanguageDefinition( "t" , "Test" , this.inlineDefinition , this.inlineDefinition );
+		LanguageDefinition language = this.createObject( xml , LanguageDefinition.class );
+		language.verifyData( );
+	}
+
+}
diff --git a/legacyworlds-server-tests/src/test/resources/.empty b/legacyworlds-server-tests/src/test/resources/.empty
new file mode 100644
index 0000000..e69de29
diff --git a/legacyworlds/doc/TODO.txt b/legacyworlds/doc/TODO.txt
index f1fc671..448622a 100644
--- a/legacyworlds/doc/TODO.txt
+++ b/legacyworlds/doc/TODO.txt
@@ -20,8 +20,6 @@ SERVER & DATABASE:
 
   * Add a tool to initialise the database
 
-  * I18N loader: improve text file loading (use relative paths)
-
   * Replace current authentication information (pair of hashes) with a
 	salted SHA512 hash.
 		-> Make sure it is still possible to import old passwords using the
diff --git a/legacyworlds/pom.xml b/legacyworlds/pom.xml
index fd00897..773e818 100644
--- a/legacyworlds/pom.xml
+++ b/legacyworlds/pom.xml
@@ -177,6 +177,11 @@
 				<artifactId>legacyworlds-server-interfaces</artifactId>
 				<version>${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build}</version>
 			</dependency>
+			<dependency>
+				<groupId>com.deepclone.lw</groupId>
+				<artifactId>legacyworlds-server-main</artifactId>
+				<version>${legacyworlds.version.main}.${legacyworlds.version.release}-${legacyworlds.version.build}</version>
+			</dependency>
 			<dependency>
 				<groupId>com.deepclone.lw</groupId>
 				<artifactId>legacyworlds-session</artifactId>