Skip to content

Commit

Permalink
22, 23
Browse files Browse the repository at this point in the history
  • Loading branch information
defuncart committed May 31, 2018
1 parent ddef3a9 commit b50c9e5
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 0 deletions.
55 changes: 55 additions & 0 deletions #22-AndroidDeviceFilter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 22 - Android Device Filter

Tucked away in **Player Settings - Android - Other Settings**, there is a *Device Filter* setting with the options **FAT (ARMv7 + x86)**, **ARMv7** and **x86**. There are many approaches to reducing the build size for Android, but one sure approach is to change the default device filter from **FAT** (which builds a combined executable compatible with both ARMv7 and x86 architectures), and build separate builds for **ARMv7** and **x86**, where the **x86** build has a higher build number than **ARMv7**. Although this requires building two builds, you will reduce the actual install sizes by about 10MB.

The Google Play Multiple APK Support state that:

> - All APKs you publish for the same application must have the same package name and be signed with the same certificate key.
> - Each APK must have a different version code, specified by the android:versionCode attribute.
For an empty project, **FAT** has an install size of 41.88Mb, while **ARMv7** had an install size of 32.23Mb. On other projects I've notice roughly a 10Mb difference also. It's basically a free trick.

Now you might be thinking that building multiple builds seems time consuming, however we can easy write a custom script to take care of that for us:

```c#
/// <summary>Builds the game for the Android platform using a menu item.</summary>
[MenuItem("Tools/Build/Android")]
public static void BuildForAndroid()
{
///arm
BuildAndroidForDevice(AndroidTargetDevice.ARMv7);
//x86
BuildAndroidForDevice(AndroidTargetDevice.x86);
}

/// <summary>Builds the game for the Android platform using a given target device.</summary>
private static void BuildAndroidForDevice(AndroidTargetDevice device)
{
PlayerSettings.Android.targetDevice = device;
string androidPath = string.Format("{0}/Builds/{1} ({2}).apk", Path.GetDirectoryName(Application.dataPath), "My App", device.ToString());
BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, androidPath, BuildTarget.Android, BuildOptions.None);
}
```

The last thing that we need to do is assign different version codes to each build. One approach is to define a large integer whose components reflect the builds major version, minor version, path version, build number and target device:

![](images/androidDeviceFilter1.png)

Thus version 1.2.3 with build version 17 for arm would be 10203170, while x86 would be 10203171. Notice that each x86 build has a higher version code than the corresponding arm build, while version 1.2.4 would have a higher version code than 1.2.3.

```c#
/// <summary>Determines the correct versionCode for Android.</summary>
/// <param name="major">The app's major version number (i.e. 1).</param>
/// <param name="minor">The app's minor version number (i.e. 0).</param>
/// <param name="patch">The app's patch version number (i.e. 0).</param>
/// <param name="build">The app's build version number (i.e. 99).</param>
/// <param name="x86">Whether it is an x86 build.</param>
private static int AndroidVersionCode(int major, int minor, int patch, int build, bool x86)
{
return major*100000 + minor*10000 + patch*1000 + build*10 + (x86 ? 1 : 0);
}
```

## Further Reading

[Android Developer - Multiple APK Support](https://developer.android.com/google/play/publishing/multiple-apks.html)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions #23-Pseudolocalization/English.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{ "items" : [
{ "key": "hello", "value": "Hello World!"},
{ "key": "test", "value": "The quick brown fox jumps over the lazy dog."}
]}
1 change: 1 addition & 0 deletions #23-Pseudolocalization/GermanPseudo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"items":[{"key":"hello","value":"Hellö Wörld!|ßüüÜß"},{"key":"test","value":"The qüick bröwn föx jümpß över the lazy dög.|ÄßÖüÜüÜÜÖÜüäää"}]}
225 changes: 225 additions & 0 deletions #23-Pseudolocalization/PseudoLocalizationWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Written by James Leahy. (c) 2017-2018 DeFunc Art.
* https://github.com/defuncart/
*/
#if UNITY_EDITOR
using DeFuncArt.Serialization;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.Assertions;

/// <summary>An EditorWindow to gerate Pseudolocalizations.</summary>
public class PseudoLocalizationWindow : EditorWindow
{
/// <summary>An enum representing the types of languages that can be rendered in Pseudotext.</summary>
public enum Language
{
German, Polish, Russian
}
/// <summary>The chosen language to render.</summary>
private Language langugage;

/// <summary>The special characters for German.</summary>
private string[] specialCharacters_DE = { "ä", "ö", "ü", "ß", "Ä", "Ö", "Ü" };
/// <summary>The special characters for Polish.</summary>
private string[] specialCharacters_PL = { "ą", "ć", "ę", "ł", "ń", "ó", "ś", "ż", "ź", "Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ż", "Ź" };
/// <summary>The special characters for Russian.</summary>
private string[] specialCharacters_RU = { "а", "б", "в", "г", "д", "е", "ё", "ж", "з", "и", "й", "к", "л", "м", "н", "о", "п", "р", "с", "т", "у", "ф", "х", "ц", "ч", "ш", "щ", "ъ", "ы", "ь", "э", "ю", "я", "А", "Б", "В", "Г", "Д", "Е", "Ё", "Ж", "З", "И", "Й", "К", "Л", "М", "Н", "О", "П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ч", "Ш", "Щ", "Ъ", "Ы", "Ь", "Э", "Ю", "Я" };
/// <summary>The special characters the selected language.</summary>
private string[] specialCharacters
{
get
{
if (langugage == Language.German) { return specialCharacters_DE; }
else if (langugage == Language.Polish) { return specialCharacters_PL; }
else { return specialCharacters_RU; }
}
}
/// <summary>A random special character for the selected language.</summary>
private string randomSpecialCharacter
{
get { return specialCharacters[Random.Range(0, specialCharacters.Length)]; }
}

/// <summary>A dictionary of mapping characters for German.</summary>
private Dictionary<string, string[]> mappingCharacters_DE = new Dictionary<string, string[]>(){
{"a" , new string[]{"ä"} }, {"A" , new string[]{"Ä"} },
{"o" , new string[]{"ö"} }, {"O" , new string[]{"Ö"} },
{"u" , new string[]{"ü"} }, {"U" , new string[]{"Ü"} },
{"s" , new string[]{"ß"} }
};
/// <summary>A dictionary of mapping characters for Polish.</summary>
private Dictionary<string, string[]> mappingCharacters_PL = new Dictionary<string, string[]>(){
{"a" , new string[]{"ą"} }, {"A" , new string[]{"Ą"} },
{"c" , new string[]{"ć"} }, {"C" , new string[]{"Ć"} },
{"e" , new string[]{"ę"} }, {"E" , new string[]{"Ę"} },
{"l" , new string[]{"ł"} }, {"L" , new string[]{"Ł"} },
{"n" , new string[]{"ń"} }, {"N" , new string[]{"Ń"} },
{"o" , new string[]{"ó"} }, {"O" , new string[]{"Ó"} },
{"s" , new string[]{"ś"} }, {"S" , new string[]{"Ś"} },
{"z" , new string[]{"ż", "ź"} }, {"Z" , new string[]{"Ż", "Ź"} }
};
/// <summary>A dictionary of mapping characters for Russian.</summary>
private Dictionary<string, string[]> mappingCharacters_RU = new Dictionary<string, string[]>(){
{"a" , new string[]{"а"} }, {"A" , new string[]{"А"} },
{"b" , new string[]{"ь", "в", "б", "ъ"} }, {"B" , new string[]{"Ь", "В", "Б", "Ъ"} },
{"c" , new string[]{"с"} }, {"C" , new string[]{"С"} },
{"d" , new string[]{"д"} }, {"D" , new string[]{"Д"} },
{"e" , new string[]{"е", "ё", "э"} }, {"E" , new string[]{"Е", "Ё", "Э"} },
{"f" , new string[]{"ф"} }, {"F" , new string[]{"Ф"} },
{"g" , new string[]{"г"} }, {"G" , new string[]{"Г"} },
{"h" , new string[]{"н"} }, {"H" , new string[]{"Н"} },
{"i" , new string[]{"и"} }, {"I" , new string[]{"И"} },
{"j" , new string[]{"й"} }, {"J" , new string[]{"Й"} },
{"k" , new string[]{"к"} }, {"K" , new string[]{"К"} },
{"l" , new string[]{"л"} }, {"L" , new string[]{"Л"} },
{"m" , new string[]{"м"} }, {"M" , new string[]{"М"} },
{"n" , new string[]{"п"} }, {"N" , new string[]{"П"} },
{"o" , new string[]{"о"} }, {"O" , new string[]{"О"} },
{"p" , new string[]{"р"} }, {"P" , new string[]{"Р"} },
{"q" , new string[]{"ч"} }, {"Q" , new string[]{"Ч"} },
{"r" , new string[]{"я"} }, {"R" , new string[]{"Я"} },
{"s" , new string[]{"з"} }, {"S" , new string[]{"З"} },
{"t" , new string[]{"т"} }, {"T" , new string[]{"Т"} },
{"u" , new string[]{"ц"} }, {"U" , new string[]{"Ц"} },
{"v" , new string[]{"ч"} }, {"V" , new string[]{"Ч"} },
{"w" , new string[]{"ш", "щ"} }, {"W" , new string[]{"Ш", "Щ"} },
{"x" , new string[]{"х", "ж"} }, {"X" , new string[]{"Х", "Ж"} },
{"y" , new string[]{"у"} }, {"Y" , new string[]{"У"} },
{"z" , new string[]{"з"} }, {"Z" , new string[]{"З"} }
};
/// <summary>A dictionary of mapping characters for the selected language.</summary>
private Dictionary<string, string[]> mappingCharacters
{
get
{
if(langugage == Language.German) { return mappingCharacters_DE; }
else if(langugage == Language.Polish) { return mappingCharacters_PL; }
else { return mappingCharacters_RU; }
}
}

/// <summary>A reference to the TextAsset JSON file with English strings.</summary>
public Object englishJSONAsset;

//add a menu item named "Pseudolocalization" to the Tools menu
[MenuItem("Tools/Pseudolocalization")]
public static void ShowWindow()
{
//show existing window instance - if one doesn't exist, create one
GetWindow(typeof(PseudoLocalizationWindow));
}

/// <summary>Draws the window.</summary>
private void OnGUI()
{
//set the label's style to wrap words
EditorStyles.label.wordWrap = true;

//draw an info label
EditorGUILayout.LabelField("Input English strings (as JSON):");

//draw the englishJSONAsset
englishJSONAsset = EditorGUILayout.ObjectField(englishJSONAsset, typeof(TextAsset), true);

//draw a space
EditorGUILayout.Space();

//draw a language popup
langugage = (Language) EditorGUILayout.EnumPopup("Language to render:", langugage);

//draw an info label
if(englishJSONAsset != null)
{
string relativeOutputFilepath = AssetDatabase.GetAssetPath(englishJSONAsset).Replace(Path.GetFileName(AssetDatabase.GetAssetPath(englishJSONAsset)), string.Format("{0}Pseudo.json", langugage.ToString()));
EditorGUILayout.LabelField(string.Format("The output file will be saved to {0}", relativeOutputFilepath));
}

//draw a space
EditorGUILayout.Space();

//draw a button which, if triggered, render the Pseudolocalization for the selected language
if(GUILayout.Button("Render Pseudolocalization")) { RenderPseudolocalization(); }
}

/// <summary>Render the Pseudolocalization for the selected language.</summary>
private void RenderPseudolocalization()
{
//display an error if there is no input file
if(englishJSONAsset == null) { Debug.LogErrorFormat("No input file."); return; }

//load the json file as a dictionary
TextAsset asset = englishJSONAsset as TextAsset;
Dictionary<string, string> inputJSON = JSONSerializer.FromJson<Dictionary<string, string>>(asset.text);
Dictionary<string, string> outputJSON = new Dictionary<string, string>();

//loop through each kvp
foreach(KeyValuePair<string, string> kvp in inputJSON)
{
//determine the pseudotranslation
string englishText = kvp.Value;
int numberOfRandomSpecialCharactersToGenerate = PseudotranslationLengthForText(englishText) - englishText.Length;
string pseudoTranslation = string.Format("{0}|{1}", AddSpecialCharactersToText(englishText), GenerateXRandomSpecialCharacters(numberOfRandomSpecialCharactersToGenerate));

//add to output dictionary
outputJSON[kvp.Key] = pseudoTranslation;
}

//convert the dictionary to json
string outputJSONString = JSONSerializer.ToJson<Dictionary<string, string>>(outputJSON);

//determine a path for the output file
string path = string.Format("{0}/{1}Pseudo.json", Path.GetFullPath(Directory.GetParent(AssetDatabase.GetAssetPath(englishJSONAsset)).FullName), langugage.ToString());

//save the file to disk
File.WriteAllText(path, outputJSONString);

//update asset database
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
}

/// <summary>Returns a string containing mapped special characters (a => ä) for the selected language.</summary>
private string AddSpecialCharactersToText(string text)
{
StringBuilder sb = new StringBuilder();
char[] characters = text.ToCharArray();
string[] keys = mappingCharacters.Keys.ToArray();
foreach(char character in characters)
{
int index = System.Array.IndexOf(keys, character.ToString());
if(index > 0)
{
string[] possibleMappings = mappingCharacters[character.ToString()];
sb.Append(possibleMappings[Random.Range(0, possibleMappings.Length)]);
}
else { sb.Append(character); }
}

return sb.ToString();
}

/// <summary>Returns a string contain X random special characters for the selected language.</summary>
private string GenerateXRandomSpecialCharacters(int count)
{
StringBuilder sb = new StringBuilder();
for(int i=0; i < count; i++)
{
sb.Append(randomSpecialCharacter);
}
return sb.ToString();
}

/// <summary>Determine the Pseudotranslation length for a given text string.</summary>
private int PseudotranslationLengthForText(string text)
{
if(text.Length > 20) { return Mathf.CeilToInt(text.Length * 1.3f); }
else if(text.Length > 10) { return Mathf.CeilToInt(text.Length * 1.4f); }
else { return Mathf.CeilToInt(text.Length * 1.5f); }
}
}
#endif
84 changes: 84 additions & 0 deletions #23-Pseudolocalization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 23 - Pseudolocalization

*Internationalization* is the process of designing a software application so that it can easily be adapted to various other languages and regions without any programming changes. *Localization* is the process of adapting internationalized software for a specific region or language by adding locale-specific components (€2.99 => 2,99€) and translating text (Hello World! => Hallo Welt!). *Pseudolocalization* is a software testing method used before the localization process in which a fake (or pseudo) translations (with the region and language specific characters) are generated: Hello World! => Hellö Wörld!|ÜüäßÖ

The benefits of pseudolocalization are three fold:

1. To test that all (special) characters of the target locale (i.e. German) are displayed correctly.
2. To test that text boxes can accommodate longer translations. If a pseduotranslation is cutoff or visually looks ugly on the screen, then there's a good chance that the real translation will also be.
3. To flag hardcoded strings or non-localized art.

## Text Expansion

Considering English as the base language, after translation many languages will exhibit *Text Expansion* and have longer text strings. Generally German extends by 10-35%, Polish 20-30% and Russian by 15%. As a quick rule of thumb, I like to utilize IGDA Localization SIG's suggestions:

| English Text Length | Pseduotranslation Length |
| :-------------------|:-------------------------|
| 1-10 characters | 150% |
| 10-20 characters | 140% |
| >20 characters | 130% |

**Note:** some languages can actually have shorter text strings. In this post I will be considering languages that generally expand.

## Pseudo text

From various examples I have seen on the web, there are many different ways to visualize these pseudo texts. Personally I prefer to generate separate pseudolocalizations for each target localization and test them separately to ensure that each target localization will be adequately rendered. My pseduotranslation style starts with the English text, replaces any Basic Latin characters (i.e. English letters) with similar special characters, uses pipe **|** as a divider, and then adds a few random characters at the end (depending on original text size.) So *Hello World!* becomes:

| English | Hello World! |
| :-------|:------------------------|
| German | Hellö Wörld!&#124;ÜüäßÖ |
| Polish | Hęłłó Wórłd!&#124;ꜿʌ |
| Russian | Нёлло Шоялд!&#124;ОТЧжт |

It is important to remember that this pseduotranslation is non-sensical: it is not a real translation, instead merely a way to test that the game is ready for the translation stage.

## Unity Helper

A pseudolocalization generator can easily be coded in Unity to enable quick testing.

![](images/pseudoLocalization1.png)

This EditorWindow is given a JSON TextAsset reference (assumed to be English) of the form

```
{ "items" : [
{ "key": "hello", "value": "Hello World!"},
{ "key": "test", "value": "The quick brown fox jumps over the lazy dog."}
]}
```
and saves, for instance, *GermanPseudo.json* to the save directory as the original file:

```
{"items":[
{ "key": "hello", "value": "Hellö Wörld!|ßüüÜß"},
{ "key": "test", "value": "The qüick bröwn föx jümpß över the lazy dög.|ÄßÖüÜüÜÜÖÜüäää"}
]}
```

This JSON is read from and written to disk using the [*JSON Serialization*](https://github.com/defuncart/50-unity-tips/tree/master/%2309-JSONSerialization) mentioned back in Tip #9.

## Other Languages

This approach could be easily extended to other Latin script languages, for instance:

| Language | Special Characters |
| :--------|:-------------------------------|
| French | àâæéèêëîïôœùûüçÀÂÆÉÈÊËÎÏÔŒÙÛÜÇ |
| Czech | áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ |
| Spanish | áéíóúüñÁÉÍÓÚÜÑ¿¡ |

I don't have any experience with languages that contract (when translated to from English) or languages that are written right-to-left, but I imagine a similar approach to Russian would work perfectly fine.

## Conclusion

The earlier that localization issues are flagged, the less time required and more cost effective the solution will be. By investing a small amount of time in generating pseudotranslations and verifying that the game is ready for localization, one can be assured that everything is in order before actually beginning the localization phase.

## Further Reading

[50 Unity Tips - JSON Serialization](https://github.com/defuncart/50-unity-tips/tree/master/%2309-JSONSerialization)

[Pseudo-Localization – A Must in Video Gaming](http://www.gamasutra.com/blogs/IGDALocalizationSIG/20180504/317560/PseudoLocalization__A_Must_in_Video_Gaming.php)

[Localization - Expansion and contraction factors](https://www.andiamo.co.uk/resources/expansion-and-contraction-factors)

[What is Pseudo-Localization?](http://blog.globalizationpartners.com/what-is-pseudo-localization.aspx)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b50c9e5

Please sign in to comment.