diff --git a/api/net/sf/briar/api/db/ContactId.java b/api/net/sf/briar/api/db/ContactId.java
index 3e793c4bfe5e1d1ee74c37a3657b6d3c8e7a5fb2..39d362a3a97eb2f843be2aac283fadc87638e3b3 100644
--- a/api/net/sf/briar/api/db/ContactId.java
+++ b/api/net/sf/briar/api/db/ContactId.java
@@ -1,6 +1,6 @@
 package net.sf.briar.api.db;
 
-/** Uniquely identifies a contact. */
+/** Type-safe wrapper for an integer that uniquely identifies a contact. */
 public class ContactId {
 
 	private final int id;
diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index 59261e2e1b9b6490259ace35300b59043a86c016..38a7bf68daa2eed9b3b50ab19c6c49456c5c6b08 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -35,7 +35,10 @@ public interface DatabaseComponent {
 	/** Adds a locally generated message to the database. */
 	void addLocallyGeneratedMessage(Message m) throws DbException;
 
-	/** Generates a bundle of messages for the given contact. */
+	/**
+	 * Generates a bundle of acknowledgements, subscriptions, and batches of
+	 * messages for the given contact.
+	 */
 	void generateBundle(ContactId c, Bundle b) throws DbException;
 
 	/** Returns the user's rating for the given author. */
@@ -45,8 +48,9 @@ public interface DatabaseComponent {
 	Set<GroupId> getSubscriptions() throws DbException;
 
 	/**
-	 * Processes a bundle of messages received from the given contact. Some
-	 * or all of the messages in the bundle may be stored.
+	 * Processes a bundle of acknowledgements, subscriptions, and batches of
+	 * messages received from the given contact. Some or all of the messages
+	 * in the bundle may be stored.
 	 */
 	void receiveBundle(ContactId c, Bundle b) throws DbException;
 
diff --git a/api/net/sf/briar/api/i18n/FontManager.java b/api/net/sf/briar/api/i18n/FontManager.java
index df6be33d5b27817411545002416c53f62337ade6..13249bb41ad4a238a124f645d2788ff70e80638e 100644
--- a/api/net/sf/briar/api/i18n/FontManager.java
+++ b/api/net/sf/briar/api/i18n/FontManager.java
@@ -1,13 +1,12 @@
 package net.sf.briar.api.i18n;
 
 import java.awt.Font;
-import java.io.IOException;
 import java.util.Locale;
 
 public interface FontManager {
 
 	/** Initializes the FontManager for the given locale. */
-	void initialize(Locale locale) throws IOException;
+	void initialize(Locale locale);
 
 	/** Returns the appropriate font for the given language. */
 	Font getFontForLanguage(String language);
diff --git a/api/net/sf/briar/api/protocol/AuthorId.java b/api/net/sf/briar/api/protocol/AuthorId.java
index 5f5ab3b1334cfff7c09d310a3fda022d35cbc9d5..8b32851a8b766e6e1198e1466decb730393c9444 100644
--- a/api/net/sf/briar/api/protocol/AuthorId.java
+++ b/api/net/sf/briar/api/protocol/AuthorId.java
@@ -2,7 +2,7 @@ package net.sf.briar.api.protocol;
 
 import java.util.Arrays;
 
-/** Uniquely identifies a pseudonymous author. */
+/** Type-safe wrapper for a byte array that uniquely identifies an author. */
 public class AuthorId {
 
 	public static final int LENGTH = 32;
diff --git a/api/net/sf/briar/api/protocol/BatchId.java b/api/net/sf/briar/api/protocol/BatchId.java
index 9aad9c0e2db61e025e3779d66b57aec354ac3d3b..4b4578b03f32ace399bd6482e9206ce861cbe8f3 100644
--- a/api/net/sf/briar/api/protocol/BatchId.java
+++ b/api/net/sf/briar/api/protocol/BatchId.java
@@ -2,7 +2,10 @@ package net.sf.briar.api.protocol;
 
 import java.util.Arrays;
 
-/** Uniquely identifies a batch of messages. */
+/**
+ * Type-safe wrapper for a byte array that uniquely identifies a batch of
+ * messages.
+ */
 public class BatchId {
 
 	public static final int LENGTH = 32;
diff --git a/api/net/sf/briar/api/protocol/BundleId.java b/api/net/sf/briar/api/protocol/BundleId.java
index 9dba86d20783c6a0ec61f238348840b58868ad72..2dd7e8f59f5483fa68cf0bc470a34eb429010b04 100644
--- a/api/net/sf/briar/api/protocol/BundleId.java
+++ b/api/net/sf/briar/api/protocol/BundleId.java
@@ -2,6 +2,10 @@ package net.sf.briar.api.protocol;
 
 import java.util.Arrays;
 
+/**
+ * Type-safe wrapper for a byte array that uniquely identifies a bundle of
+ * acknowledgements, subscriptions, and batches of messages.
+ */
 public class BundleId {
 
 	public static final BundleId NONE = new BundleId(new byte[] {
diff --git a/api/net/sf/briar/api/protocol/GroupId.java b/api/net/sf/briar/api/protocol/GroupId.java
index 75a12c8a3f6222142647fda4db8d3854a40d6e6f..46c0456970c737cfaf5d057fe752f012750f4f86 100644
--- a/api/net/sf/briar/api/protocol/GroupId.java
+++ b/api/net/sf/briar/api/protocol/GroupId.java
@@ -2,7 +2,10 @@ package net.sf.briar.api.protocol;
 
 import java.util.Arrays;
 
-/** Uniquely identifies a group to which a user may subscribe. */
+/**
+ * Type-safe wrapper for a byte array that uniquely identifies a group to which
+ * users may subscribe.
+ */
 public class GroupId {
 
 	public static final int LENGTH = 32;
diff --git a/api/net/sf/briar/api/protocol/MessageId.java b/api/net/sf/briar/api/protocol/MessageId.java
index ad25c171d6920ebda3238cfe9e62c01305564ca1..ee25caddc0f9a4e2b9b107e1b3c91859b6e0a350 100644
--- a/api/net/sf/briar/api/protocol/MessageId.java
+++ b/api/net/sf/briar/api/protocol/MessageId.java
@@ -2,7 +2,7 @@ package net.sf.briar.api.protocol;
 
 import java.util.Arrays;
 
-/** Uniquely identifies a message. */
+/** Type-safe wrapper for a byte array that uniquely identifies a message. */
 public class MessageId {
 
 	/** Used to indicate that the first message in a thread has no parent. */
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index ed6ba26912d3a307d4c4a988b672c14b329e0da5..2ed2467bb9ae93c985cd3a465ae769f2c8e912b7 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -15,6 +15,10 @@ import net.sf.briar.api.protocol.MessageId;
 
 import com.google.inject.Provider;
 
+/**
+ * Abstract superclass containing code shared by ReadWriteLockDatabaseComponent
+ * and SynchronizedDatabaseComponent.
+ */
 abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 
 	private static final Logger LOG =
@@ -35,9 +39,17 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		startCleaner();
 	}
 
+	/**
+	 * Removes the oldest messages from the database, with a total size less
+	 * than or equal to the given size.
+	 */
 	protected abstract void expireMessages(long size) throws DbException;
 
-	// Locking: messages write
+	/**
+	 * Calculates and returns the sendability score of a message.
+	 * <p>
+	 * Locking: messages write.
+	 */
 	private int calculateSendability(Txn txn, Message m) throws DbException {
 		int sendability = 0;
 		// One point for a good rating
@@ -51,6 +63,12 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		return sendability;
 	}
 
+	/**
+	 * Checks how much free storage space is available to the database, and if
+	 * necessary expires old messages until the free space is at least
+	 * MIN_FREE_SPACE. While the free space is less than CRITICAL_FREE_SPACE,
+	 * operations that attempt to store messages in the database will block.
+	 */
 	private void checkFreeSpaceAndClean() throws DbException {
 		long freeSpace = db.getFreeSpace();
 		while(freeSpace < MIN_FREE_SPACE) {
@@ -74,7 +92,11 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		}
 	}
 
-	// Locking: contacts read
+	/**
+	 * Returns true iff the database contains the given contact.
+	 * <p>
+	 * Locking: contacts read.
+	 */
 	protected boolean containsContact(ContactId c) throws DbException {
 		Txn txn = db.startTransaction();
 		try {
@@ -87,14 +109,24 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		}
 	}
 
-	// Locking: contacts read, messages write, messageStatuses write
+	/**
+	 * Removes the given message (and all associated state) from the database. 
+	 * <p>
+	 * Locking: contacts read, messages write, messageStatuses write.
+	 */
 	protected void removeMessage(Txn txn, MessageId id) throws DbException {
 		Integer sendability = db.getSendability(txn, id);
 		assert sendability != null;
+		// If the message is sendable, deleting it may affect its ancestors'
+		// sendability (backward inclusion)
 		if(sendability > 0) updateAncestorSendability(txn, id, false);
 		db.removeMessage(txn, id);
 	}
 
+	/**
+	 * Returns true iff the amount of free storage space available to the
+	 * database should be checked.
+	 */
 	private boolean shouldCheckFreeSpace() {
 		synchronized(spaceLock) {
 			long now = System.currentTimeMillis();
@@ -117,6 +149,12 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		return false;
 	}
 
+	/**
+	 * Starts a new thread to monitor the amount of free storage space
+	 * available to the database and expire old messages as necessary.
+	 * <p>
+	 * FIXME: The thread implementation should be factored out.
+	 */
 	private void startCleaner() {
 		Runnable cleaner = new Runnable() {
 			public void run() {
@@ -140,7 +178,14 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		new Thread(cleaner).start();
 	}
 
-	// Locking: contacts read, messages write, messageStatuses write
+	/**
+	 * If the given message is already in the database, marks it as seen by the
+	 * sender and returns false. Otherwise stores the message, updates the
+	 * sendability of its ancestors if necessary, marks the message as seen by
+	 * the sender and unseen by all other contacts, and returns true.
+	 * <p>
+	 * Locking: contacts read, messages write, messageStatuses write.
+	 */
 	protected boolean storeMessage(Txn txn, Message m, ContactId sender)
 	throws DbException {
 		boolean added = db.addMessage(txn, m);
@@ -164,7 +209,15 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		return added;
 	}
 
-	// Locking: messages write
+	/**
+	 * Iteratively updates the sendability of a message's ancestors to reflect
+	 * a change in the message's sendability. Returns the number of ancestors
+	 * that have changed from sendable to not sendable, or vice versa.
+	 * <p>
+	 * Locking: messages write.
+	 * @param increment True if the message's sendability has changed from 0 to
+	 * greater than 0, or false if it has changed from greater than 0 to 0.
+	 */
 	private int updateAncestorSendability(Txn txn, MessageId m,
 			boolean increment) throws DbException {
 		int affected = 0;
@@ -191,7 +244,14 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 		return affected;
 	}
 
-	// Locking: messages write
+	/**
+	 * Updates the sendability of all messages written by the given author, and
+	 * the ancestors of those messages if necessary.
+	 * <p>
+	 * Locking: messages write.
+	 * @param increment True if the user's rating for the author has changed
+	 * from not good to good, or false if it has changed from good to not good.
+	 */
 	protected void updateAuthorSendability(Txn txn, AuthorId a,
 			boolean increment) throws DbException {
 		int direct = 0, indirect = 0;
@@ -217,6 +277,11 @@ abstract class DatabaseComponentImpl<Txn> implements DatabaseComponent {
 				+ indirect + " indirectly");
 	}
 
+	/**
+	 * Blocks until messages are allowed to be stored in the database. The
+	 * storage of messages is not allowed while the amount of free storage
+	 * space available to the database is less than CRITICAL_FREE_SPACE.
+	 */
 	protected void waitForPermissionToWrite() {
 		synchronized(writeLock) {
 			while(!writesAllowed) {
diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
index f6d874fbe9aa024f826e26a28f42fda475b3fe9a..c1ead3ce907a4d1f5e754947276e91a9c9c745d9 100644
--- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
+++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java
@@ -21,6 +21,10 @@ import net.sf.briar.api.protocol.MessageId;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+/**
+ * An implementation of DatabaseComponent using reentrant read-write locks.
+ * This implementation can allow writers to starve.
+ */
 class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 
 	private static final Logger LOG =
@@ -28,8 +32,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 
 	/*
 	 * Locks must always be acquired in alphabetical order. See the Database
-	 * interface to find out which calls require which locks. Note: this
-	 * implementation can allow writers to starve.
+	 * interface to find out which calls require which locks.
 	 */
 
 	private final ReentrantReadWriteLock contactLock =
@@ -112,6 +115,8 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 					try {
 						Txn txn = db.startTransaction();
 						try {
+							// Don't store the message if the user has
+							// unsubscribed from the group
 							if(db.containsSubscription(txn, m.getGroup())) {
 								boolean added = storeMessage(txn, m, null);
 								assert added;
diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
index 6e514a2c48eaa5ef45e56a335b46b47497b5ff14..b28841598baefa9b4cc00bad2afd131d9e3a5170 100644
--- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
+++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java
@@ -20,6 +20,10 @@ import net.sf.briar.api.protocol.MessageId;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+/**
+ * An implementation of DatabaseComponent using Java synchronization. This
+ * implementation does not distinguish between readers and writers.
+ */
 class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 
 	private static final Logger LOG =
@@ -80,6 +84,8 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> {
 					synchronized(subscriptionLock) {
 						Txn txn = db.startTransaction();
 						try {
+							// Don't store the message if the user has
+							// unsubscribed from the group
 							if(db.containsSubscription(txn, m.getGroup())) {
 								boolean added = storeMessage(txn, m, null);
 								assert added;
diff --git a/components/net/sf/briar/i18n/FontManagerImpl.java b/components/net/sf/briar/i18n/FontManagerImpl.java
index 378cd354c26a7da87648671f0c957581ee28029c..e4877d59607f83b1a30dfd6848529bf5325c953d 100644
--- a/components/net/sf/briar/i18n/FontManagerImpl.java
+++ b/components/net/sf/briar/i18n/FontManagerImpl.java
@@ -12,6 +12,8 @@ import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.swing.UIManager;
 
@@ -20,19 +22,32 @@ import net.sf.briar.util.FileUtils;
 
 public class FontManagerImpl implements FontManager {
 
+	private static final Logger LOG =
+		Logger.getLogger(FontManagerImpl.class.getName());
+
+	/**
+	 * Each bundled font is associated with a size, which is meant to occupy
+	 * roughly the same amount of space as the default font (12 point sans),
+	 * and a list of languages for which the bundled font should be used.
+	 */
 	private static final BundledFont[] BUNDLED_FONTS = {
+		// Use TibetanMachineUni for Tibetan
 		new BundledFont("TibetanMachineUni.ttf", 14f, new String[] { "bo" }),
+		// Use Padauk for Burmese
 		new BundledFont("Padauk.ttf", 14f, new String[] { "my" }),
 	};
 
+	// Map from languages to fonts
 	private final Map<String, Font> fonts = new TreeMap<String, Font>();
 
 	private volatile Font defaultFont = null, uiFont = null;
 
-	public void initialize(Locale locale) throws IOException {
-		try {
-			ClassLoader loader = getClass().getClassLoader();
-			for(BundledFont bf : BUNDLED_FONTS) {
+	public void initialize(Locale locale) {
+		// Look for bundled fonts in the jar and the filesystem. If any fonts
+		// are missing or fail to load, fall back to the default font.
+		ClassLoader loader = getClass().getClassLoader();
+		for(BundledFont bf : BUNDLED_FONTS) {
+			try {
 				InputStream in = loader.getResourceAsStream(bf.filename);
 				if(in == null) {
 					File root = FileUtils.getBriarDirectory();
@@ -42,9 +57,13 @@ public class FontManagerImpl implements FontManager {
 				Font font = Font.createFont(Font.TRUETYPE_FONT, in);
 				font = font.deriveFont(bf.size);
 				for(String language : bf.languages) fonts.put(language, font);
+			} catch(FontFormatException e) {
+				if(LOG.isLoggable(Level.WARNING))
+					LOG.warning("Could not load font: " + e.getMessage());
+			} catch(IOException e) {
+				if(LOG.isLoggable(Level.WARNING))
+					LOG.warning("Could not load font: " + e.getMessage());
 			}
-		} catch(FontFormatException e) {
-			throw new IOException(e);
 		}
 		defaultFont = getFont("Sans", 12f);
 		assert defaultFont != null; // FIXME: This is failing on Windows
diff --git a/components/net/sf/briar/i18n/I18nImpl.java b/components/net/sf/briar/i18n/I18nImpl.java
index af09abf9d062891d52ac846662fcef4c5b8331ac..f4095a35940de9ddffd77de2c0448ffe391f4376 100644
--- a/components/net/sf/briar/i18n/I18nImpl.java
+++ b/components/net/sf/briar/i18n/I18nImpl.java
@@ -23,6 +23,11 @@ import net.sf.briar.util.FileUtils;
 
 public class I18nImpl implements I18n {
 
+	/**
+	 * Property keys for strings used in the JRE's built-in dialogs. Values
+	 * assigned to these keys in i18n properties files will override the
+	 * built-in values.
+	 */
 	private static final String[] uiManagerKeys = {
 		"FileChooser.acceptAllFileFilterText",
 		"FileChooser.cancelButtonText",
@@ -87,6 +92,7 @@ public class I18nImpl implements I18n {
 			synchronized(bundleLock) {
 				if(bundle == null) {
 					bundle = ResourceBundle.getBundle("i18n", locale, loader);
+					assert bundle != null;
 					for(String key : uiManagerKeys) {
 						try {
 							UIManager.put(key, bundle.getString(key));
@@ -106,6 +112,7 @@ public class I18nImpl implements I18n {
 		Font uiFont = fontManager.getUiFont();
 		synchronized(bundleLock) {
 			this.locale = locale;
+			Locale.setDefault(locale);
 			bundle = null;
 			synchronized(listeners) {
 				for(Listener l : listeners) l.localeChanged(uiFont);
@@ -116,7 +123,7 @@ public class I18nImpl implements I18n {
 	public void loadLocale() throws IOException {
 		File root = FileUtils.getBriarDirectory();
 		Scanner s = new Scanner(new File(root, "Data/locale.cfg"));
-		if(s.hasNextLine()) locale = new Locale(s.nextLine());
+		if(s.hasNextLine()) setLocale(new Locale(s.nextLine()));
 		s.close();
 	}
 
diff --git a/components/net/sf/briar/invitation/InvitationWorker.java b/components/net/sf/briar/invitation/InvitationWorker.java
index ce3c5751cc7f153397049bd3345e336b97951a7a..e9ef25eef2b7bdef2cff85005c56131e6a320182 100644
--- a/components/net/sf/briar/invitation/InvitationWorker.java
+++ b/components/net/sf/briar/invitation/InvitationWorker.java
@@ -41,18 +41,11 @@ class InvitationWorker implements Runnable {
 		List<File> files = new ArrayList<File>();
 		try {
 			if(callback.isCancelled()) return;
-			File invitationDat = createInvitationDat(dir);
-			files.add(invitationDat);
+			files.add(createInvitationDat(dir));
 			if(callback.isCancelled()) return;
-			if(parameters.shouldCreateExe()) {
-				File briarExe = createBriarExe(dir);
-				files.add(briarExe);
-			}
+			if(parameters.shouldCreateExe()) files.add(createBriarExe(dir));
 			if(callback.isCancelled()) return;
-			if(parameters.shouldCreateJar()) {
-				File briarJar = createBriarJar(dir);
-				files.add(briarJar);
-			}
+			if(parameters.shouldCreateJar()) files.add(createBriarJar(dir));
 		} catch(IOException e) {
 			callback.error(e.getMessage());
 			return;
@@ -69,8 +62,7 @@ class InvitationWorker implements Runnable {
 		// FIXME: Create a real invitation
 		try {
 			Thread.sleep(2000);
-		} catch(InterruptedException ignored) {
-		}
+		} catch(InterruptedException ignored) {}
 		Arrays.fill(password, (char) 0);
 		FileOutputStream out = new FileOutputStream(invitationDat);
 		byte[] buf = new byte[1024];
diff --git a/components/net/sf/briar/protocol/BatchImpl.java b/components/net/sf/briar/protocol/BatchImpl.java
index be84a7594f6b26f1f3fc719483ff61495c49a179..68d0e12ddc1e7dc000d025be5dbee34e4b38ce91 100644
--- a/components/net/sf/briar/protocol/BatchImpl.java
+++ b/components/net/sf/briar/protocol/BatchImpl.java
@@ -8,6 +8,7 @@ import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Message;
 
+/** A simple in-memory implementation of a batch. */
 class BatchImpl implements Batch {
 
 	private final List<Message> messages = new ArrayList<Message>();
diff --git a/test/build.xml b/test/build.xml
index 9c90d42b1299ec8fe1954b8264ed9e8068abcbc8..17dd1da67a7f0693cfd41abd1628f8a4131e8311 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -1,7 +1,7 @@
 <project name='test' default='test'>
 	<import file='../build-common.xml'/>
 	<target name='test' depends='depend'>
-		<junit printsummary='on'>
+		<junit printsummary='on' showoutput='true'>
 			<classpath>
 				<fileset refid='bundled-jars'/>
 				<fileset refid='test-jars'/>
@@ -10,6 +10,8 @@
 				<path refid='test-classes'/>
 				<path refid='util-classes'/>
 			</classpath>
+			<test name='net.sf.briar.i18n.FontManagerImplTest'/>
+			<test name='net.sf.briar.invitation.InvitationWorkerTest'/>
 			<test name='net.sf.briar.setup.SetupWorkerTest'/>
 			<test name='net.sf.briar.util.FileUtilsTest'/>
 			<test name='net.sf.briar.util.StringUtilsTest'/>
diff --git a/test/net/sf/briar/TestUtils.java b/test/net/sf/briar/TestUtils.java
index 86fb377099a0efa52e278be1beaf74b491c3b795..bd8397c85c1da84b6cd8d0032b5b8b32ef404908 100644
--- a/test/net/sf/briar/TestUtils.java
+++ b/test/net/sf/briar/TestUtils.java
@@ -11,7 +11,7 @@ public class TestUtils {
 	private static final AtomicInteger nextTestDir =
 		new AtomicInteger((int) (Math.random() * 1000 * 1000));
 
-	public static void delete(File f) throws IOException {
+	public static void delete(File f) {
 		if(f.isDirectory()) for(File child : f.listFiles()) delete(child);
 		f.delete();
 	}
@@ -29,4 +29,8 @@ public class TestUtils {
 		File testDir = new File("test.tmp/" + name);
 		return testDir;
 	}
+
+	public static void deleteTestDirectories() {
+		delete(new File("test.tmp"));
+	}
 }
diff --git a/test/net/sf/briar/i18n/FontManagerImplTest.java b/test/net/sf/briar/i18n/FontManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c5dbd67593ac812d97cdf3ac83a06e08730a7ab
--- /dev/null
+++ b/test/net/sf/briar/i18n/FontManagerImplTest.java
@@ -0,0 +1,69 @@
+package net.sf.briar.i18n;
+import java.awt.Font;
+import java.util.Locale;
+
+import junit.framework.TestCase;
+import net.sf.briar.i18n.FontManagerImpl;
+
+import org.junit.Test;
+
+public class FontManagerImplTest extends TestCase {
+
+	@Test
+	public void testBundledFontsAreLoaded() {
+		FontManagerImpl fontManager = new FontManagerImpl();
+		fontManager.initialize(Locale.UK);
+
+		Font font = fontManager.getFontForLanguage("en"); // English
+		assertEquals(12, font.getSize());
+
+		font = fontManager.getFontForLanguage("bo"); // Tibetan
+		assertEquals("Tibetan Machine Uni", font.getFamily());
+		assertEquals(14, font.getSize());
+
+		font = fontManager.getFontForLanguage("my"); // Burmese
+		assertEquals("Padauk", font.getFamily());
+		assertEquals(14, font.getSize());
+	}
+
+	@Test
+	public void testInternationalCharactersCanBeDisplayed() {
+		FontManagerImpl fontManager = new FontManagerImpl();
+		fontManager.initialize(Locale.UK);
+
+		Font font = fontManager.getFontForLanguage("en"); // English
+		assertTrue(font.canDisplay('a'));
+
+		font = fontManager.getFontForLanguage("ar"); // Arabic
+		assertTrue(font.canDisplay('\u0627')); // An Arabic character
+
+		font = fontManager.getFontForLanguage("bo"); // Tibetan
+		assertTrue(font.canDisplay('\u0f00')); // A Tibetan character
+
+		font = fontManager.getFontForLanguage("fa"); // Persian
+		assertTrue(font.canDisplay('\ufb56')); // A Persian character
+
+		font = fontManager.getFontForLanguage("my"); // Burmese
+		assertTrue(font.canDisplay('\u1000')); // A Burmese character
+
+		font = fontManager.getFontForLanguage("zh"); // Chinese
+		assertTrue(font.canDisplay('\u4e00')); // A Chinese character
+	}
+
+	@Test
+	public void testSetAndGetUiFont() {
+		FontManagerImpl fontManager = new FontManagerImpl();
+		fontManager.initialize(Locale.UK);
+		Font font = fontManager.getUiFont();
+		assertEquals(12, font.getSize());
+
+		fontManager.setUiFontForLanguage("bo");
+		font = fontManager.getUiFont();
+		assertEquals("Tibetan Machine Uni", font.getFamily());
+		assertEquals(14, font.getSize());
+
+		fontManager.setUiFontForLanguage("en");
+		font = fontManager.getUiFont();
+		assertEquals(12, font.getSize());
+	}
+}
diff --git a/test/net/sf/briar/invitation/InvitationWorkerTest.java b/test/net/sf/briar/invitation/InvitationWorkerTest.java
index 2424580ef56d27c13c50d4e80ad64ca0878d1e8a..3090f71f67c48129e681dd08e52dfb6b1dcb109d 100644
--- a/test/net/sf/briar/invitation/InvitationWorkerTest.java
+++ b/test/net/sf/briar/invitation/InvitationWorkerTest.java
@@ -147,7 +147,7 @@ public class InvitationWorkerTest extends TestCase {
 	}
 
 	@After
-	public void tearDown() throws IOException {
-		TestUtils.delete(testDir);
+	public void tearDown() {
+		TestUtils.deleteTestDirectories();
 	}
 }
diff --git a/test/net/sf/briar/setup/SetupWorkerTest.java b/test/net/sf/briar/setup/SetupWorkerTest.java
index cf164a9591e8233c1e3983f5dc36ea7fff76bf7e..f311b8386928b8c5edc5855d0789099dfaa413b5 100644
--- a/test/net/sf/briar/setup/SetupWorkerTest.java
+++ b/test/net/sf/briar/setup/SetupWorkerTest.java
@@ -165,7 +165,7 @@ public class SetupWorkerTest extends TestCase {
 	}
 
 	@After
-	public void tearDown() throws IOException {
-		TestUtils.delete(testDir);
+	public void tearDown() {
+		TestUtils.deleteTestDirectories();
 	}
 }
diff --git a/test/net/sf/briar/util/FileUtilsTest.java b/test/net/sf/briar/util/FileUtilsTest.java
index 8dbbe0f367883712d498482f30daa5332baa86f9..8874a306b8b69698104d12aac67dd7537bb7dbc5 100644
--- a/test/net/sf/briar/util/FileUtilsTest.java
+++ b/test/net/sf/briar/util/FileUtilsTest.java
@@ -159,7 +159,7 @@ public class FileUtilsTest extends TestCase {
 	}
 
 	@After
-	public void tearDown() throws IOException {
-		TestUtils.delete(testDir);
+	public void tearDown() {
+		TestUtils.deleteTestDirectories();
 	}
 }
diff --git a/test/net/sf/briar/util/ZipUtilsTest.java b/test/net/sf/briar/util/ZipUtilsTest.java
index c2ad772aef9687654b3f2e6439f8fd8d8f43a0a4..613c87cd74cd6ff7a3ec84bf6a0bdfb4aff94b13 100644
--- a/test/net/sf/briar/util/ZipUtilsTest.java
+++ b/test/net/sf/briar/util/ZipUtilsTest.java
@@ -195,7 +195,7 @@ public class ZipUtilsTest extends TestCase {
 	}
 
 	@After
-	public void tearDown() throws IOException {
-		TestUtils.delete(testDir);
+	public void tearDown() {
+		TestUtils.deleteTestDirectories();
 	}
 }
diff --git a/util/net/sf/briar/util/FileUtils.java b/util/net/sf/briar/util/FileUtils.java
index 74757138921aa022887affa7268ef733fab9b49a..e8052c79f494e1b55b0fd5ea5153e9e6b6801863 100644
--- a/util/net/sf/briar/util/FileUtils.java
+++ b/util/net/sf/briar/util/FileUtils.java
@@ -18,19 +18,12 @@ public class FileUtils {
 		assert f.exists();
 		if(f.isFile()) {
 			// Running from a jar - return the jar's grandparent
-			try {
-				f = f.getCanonicalFile().getParentFile().getParentFile();
-			} catch(IOException e) {
-				throw new RuntimeException(e);
-			}
+			f = f.getParentFile().getParentFile();
 		} else {
-			// Running from Eclipse
-			try {
-				f = new File(f.getCanonicalFile().getParentFile(), "Briar");
-			} catch(IOException e) {
-				throw new RuntimeException(e);
-			}
-			f.mkdir();
+			// Running from Eclipse or ant
+			f = new File(f.getParentFile(), "Briar");
+			if(!f.exists())
+				f = new File(f.getParentFile().getParentFile(), "Briar"); // Ant
 		}
 		assert f.exists();
 		assert f.isDirectory();