David King

full stack developer

All Blog Articles

Prev « The Inverse Kinematics of Lord Mountbutton Next » Complexity


Practical Internationalisation

11 May 2015

The biggest part of the latest release of Button Up! (v0.9.9) was translating the game to 11 new languages. The first stage was being able to load the game with a language pre-selected, then I decided I wanted to be able to toggle the language in-game, which meant that all "static" text must be dynamic.

The game auto-detects the users language, but also allows new languages to be manually selected.

A few moving parts

When it came to translating, the planning and programming was straightforward, as was getting machine translations, here's the basic requirements:

  1. I need seperate JSON assets for each translation, ie:
    • en.json
    • fr.json
    • de.json
    • ...
  2. I must programatically download translation data to create the JSON assets
    • Google Sheets is ideal as it can expose data in simple formats
    • Google Sheets allows anyone to edit online
  3. I should auto-detect the users language preference
    • GameClosure exposes the javascript property navigator.language
    • ...which maps to Locale on Android
    • ...and NSLocale on iOS
  4. I need to create a lang object with exposes some simple methods, ie:
    • lang.setLang('auto')
    • lang.setLang('de')
    • lang.getLanguageCode()
    • lang.getAvailableLanguages()
    • lang.get('notification_full_lives')

Converting Google Sheets into JSON

Our Google Sheet is setup with the languages as columns, and the keys as rows. In the title I've also added the language name, code and native name for reference:

The Button Up! translations, view in new tab for easier reading

Pulling the data down into individual JSON assets is straightforward:

My PHP code is as follows:

<?php
$url    = 'https://docs.google.com/spreadsheet/pub?key=1VkwercRy30p6XVQj7bITTTuQMRJBWFuj509jc9tQeqg&single=true&gid=0&output=csv';
$data   = array(); // Our structured data
$langs  = array(); // Used to lookup the language related with a column number
$native = array(); // List of language names, in their native language
$key    = false;
$rowNum = 1;
if(($handle = fopen($url, 'r')) !== FALSE){
	while(($row = fgetcsv($handle, 10000, ',')) !== FALSE){
		$cols = count($row);
		switch($rowNum){
			case 1:
				// First Row is the English name of the language, ie: French, German, Italian
				//   Do nothing
				break;
			case 2:
				// Second Row is the language tag, ie: en, es, fr
				for($c=1; $c<$cols; $c++){
					$langs[$c]      = $row[$c]; // 1=en, 2=es...
					$data[$row[$c]] = array();  // en={}, es={}
				}
				break;
			case 3:
				// Third Row is the language tag, ie: en, es, fr
				for($c=1; $c<$cols; $c++){
					$lang = $langs[$c];
					$native[$lang] = $row[$c];  // en=English, es=Español, fr=Français
				}
				break;
			default:
				// All remaining rows are the key:value store
				// The first column is the key - if it's BLANK then it's just representing more values for last key
				if($row[0]){
					$key = $row[0];
					foreach($data AS $k => $v){
						$data[$k][$key] = array();
					}
				}
				// Loop the columns, append to the translation arrays
				for($c=1; $c<$cols; $c++){
					$lang = $langs[$c];
					$data[$lang][$key][] = $row[$c];
				}
				break;

		}
		$rowNum++;
	}
	fclose($handle);
	// Process the data, tidying up blanks etc.
	foreach($data AS $lang => &$props){
		foreach($props AS $key => &$value){
			$value = array_filter($value);      // Remove empty values from the array
			if(count($value) == 0){
				unset($data[$lang][$key]);      // Blank arrays are removed
			} else if(count($value) == 1){
				$data[$lang][$key] = $value[0]; // Arrays with one element are converted to strings
			}
		}
	}
	unset($props);
	// Clear out the old JSON files
	$files = glob('resources/lang/*');
	foreach($files as $file){
		if(is_file($file)){
			unlink($file);
		}
	}
	// Great, now create the JSON files
	foreach($data AS $lang => $props){
		file_put_contents("resources/lang/{$lang}.json", json_encode($props, JSON_PRETTY_PRINT));
	}
	// And the languages
	file_put_contents('resources/lang/langs.json', json_encode($native, JSON_PRETTY_PRINT));
}

To update the game with new translations, simply add the translations to the sheet, then run the script. Smashing.

Getting the translations

Initially I wanted to use Unbabel to have human translations, but ended up just using a free Google Sheet addon called Translate My Sheet (Add-Ons > Get add-ons > search for "translate") - I'll probably move to Unbabel in due course, I just need to take a moment to read their documentation.

I've not had any native speakers take a look at the translations, and I'm sure that Google has done some strange things, but what the hell, it's in the wild now! If anyone can cast a knowing eye over the game, I'd be very grateful!


Prev « The Inverse Kinematics of Lord Mountbutton Next » Complexity