Skip to content

Instantly share code, notes, and snippets.

@hww
Last active July 30, 2019 07:54
Show Gist options
  • Save hww/130fdc912f5ae44869366de131c9a728 to your computer and use it in GitHub Desktop.
Save hww/130fdc912f5ae44869366de131c9a728 to your computer and use it in GitHub Desktop.
Simple localization solution for Unity engine
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System;
/// <summary>
/// Simple localization solution for Unity engine.
/// Loading and parsing language.po.txt file from streaming assets folder.
///
/// Author: Valery https://github.com/hww
///
/// N.B. Do not support pluralization syntax with squared brackets msgstr[0].
/// Use instead two separated entries:
///
/// msgid "singular id"
/// msgstr "singular text"
/// msgid "plural id 0"
/// msgstr "plurral text 0"
/// ...
/// msgid "plural id n"
/// msgstr "plurral text n"
///
/// </summary>
/// <example>
/// var pofile = new POFile();
/// pofile.LoadLanguage("en");
///
/// To get string:
///
/// var msg1 = pofile.GetText("global.hello_world");
/// var msg2 = pofile.GetText("global.insert_x_coins", coinsCount);
///
/// The last line is equivalent of:
///
/// var key = pofile.GetPluralKey("global.insert_x_coins", coinsCount);
/// var msg2 = pofile.GetText(key);
///
/// Example of string with pluralization:
///
/// msgid "player.insert_x_coins_0"
/// msgstr "Insert {0} Coin"
/// msgid "player.insert_x_coins_1"
/// msgstr "Insert {0} Coins"
///
/// The suffx _0 or _1 generated by method suffixMethod. The table suffixMethodsDictionary contains list
/// of suffixMethods for each language.
/// </example>
/// <see cref="https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files"/>
public class POFile
{
/// <summary>
/// Read and parse .po file for selected language.
/// The file path format is "Streming Assets/languages/{language}.po.txt"
/// </summary>
/// <param name="lang">language name aka: "en", "ru" or "en_US"</param>
public void LoadLanguage(string lang)
{
SetLanguage(lang);
string fullpath = Application.streamingAssetsPath + "/languages/" + lang + ".po.txt";
var textAsset = File.ReadAllText(fullpath);
if (textAsset == null)
{
Debug.LogError($"[POFile] '{fullpath}' file not found.");
}
else
{
Debug.Log($"[POFile] loading: '{fullpath}'");
stringsDictionary.Clear();
StringReader reader = new StringReader(textAsset);
string key = null;
string val = null;
string line;
while ((line = reader.ReadLine()) != null)
{
if (line.StartsWith("msgid \""))
{
key = RemoveQuotes(line, 5);
}
else if (line.StartsWith("msgstr \""))
{
val = RemoveQuotes(line, 6);
if (key != null && val != null)
{
// TODO: add error handling here in case of duplicate keys
stringsDictionary.Add(key, val);
key = val = null;
}
else
Debug.LogError($"[POFile] key or value is null key:'{key}' value:'{val}'.");
}
}
reader.Close();
}
}
// Set current language
private void SetLanguage(string language)
{
currentLanguage = language;
suffixMethod = suffixMethodsDictionary[language];
Debug.Assert(suffixMethod != null);
}
// Remove spaces ' ' and quotes '"' from begin and endo of string
private string RemoveQuotes(string str, int start)
{
Debug.Assert(str != null);
Debug.Assert(start < str.Length);
int startQuote = 0;
int endQuote = 0;
for (var i = start; i < str.Length; i++)
{
if (str[i] == ' ') continue;
if (str[i] == '"')
{
startQuote = i;
break;
}
Debug.LogError($"Text does not have start quote: '{str}'");
return str;
}
for (var i = str.Length - 1; i >= startQuote; i--)
{
if (str[i] == ' ') continue;
if (str[i] == '"')
{
endQuote = i;
break;
}
Debug.LogError($"Text does not have end quote: '{str}'");
return str;
}
Debug.Assert(endQuote > startQuote);
return str.Substring(startQuote + 1, (endQuote - startQuote) - 1);
}
/// <summary>Find text value with the key</summary>
public string GetText(string key)
{
Debug.Assert(key != null);
string result = "";
return stringsDictionary.TryGetValue(key, out result) ? result : null;
}
/// <summary>Find text value with the key and quantity</summary>
public string GetText(string key, int quantity)
{
Debug.Assert(key != null);
string pluralKey = GetPluralKey(key, quantity);
string result = "";
return stringsDictionary.TryGetValue(pluralKey, out result) ? result : null;
}
/// <summary>
/// Get key for pluralized text value
/// </summary>
/// <param name="key"></param>
/// <param name="quantity"></param>
/// <returns></returns>
public string GetPluralKey(string key, int quantity)
{
return $"{key}_{suffixMethod.Invoke(quantity)}";
}
/// <summary>Contains current language</summary>
public string currentLanguage = "en";
/// <summary>Keep the method for computing pluralization suffix</summary>
private Func<int, int> suffixMethod;
/// <summary>Key value pair from .po file</summary>
private Dictionary<string, string> stringsDictionary = new Dictionary<string, string>(100);
/// <summary>
/// Contains list of methos for each available language.
/// </summary>
private static readonly Dictionary<string, Func<int, int>> suffixMethodsDictionary = new Dictionary<string, Func<int, int>>()
{
{ "ar", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "bg", (n) => n != 1 ? 1 : 0 }, // Bulgarian
{ "ca", (n) => n != 1 ? 1 : 0 }, // Catalan
{ "zh-CHS", (n) => 0 }, // Chinese
{ "cs", (n) => (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 }, // Czech
{ "da", (n) => n != 1 ? 1 : 0 }, // Danish
{ "de", (n) => n != 1 ? 1 : 0 }, // German
{ "el", (n) => n != 1 ? 1 : 0 }, // Greek
{ "en", (n) => n != 1 ? 1 : 0 }, // English
{ "es", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "fi", (n) => n != 1 ? 1 : 0 }, // Finnish
{ "fr", (n) => n > 1 ? 1 : 0 }, // French
{ "he", (n) => n != 1 ? 1 : 0 }, // Hebrew
{ "hu", (n) => n != 1 ? 1 : 0 }, // Hungarian
{ "is", (n) => (n % 10 != 1 || n % 100 == 11) ? 1 : 0 }, // Icelandic
{ "it", (n) => n != 1 ? 1 : 0 }, // Italian
{ "ja", (n) => 0 }, // Japanese
{ "ko", (n) => 0 }, // Korean
{ "nl", (n) => n != 1 ? 1 : 0 }, // Dutch
{ "no", (n) => n != 1 ? 1 : 0 }, // Norwegian
{ "pl", (n) => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Polish
{ "pt", (n) => n != 1 ? 1 : 0 }, // Portuguese
{ "ro", (n) => n == 1 ? 0 : (n == 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2 }, // Romanian
{ "ru", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Russian
{ "hr", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Croatian
{ "sk", (n) => (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 }, // Slovak
{ "sq", (n) => n != 1 ? 1 : 0 }, // Albanian
{ "sv", (n) => n != 1 ? 1 : 0 }, // Swedish
{ "th", (n) => 0 }, // Thai
{ "tr", (n) => n > 1 ? 1 : 0 }, // Turkish
{ "id", (n) => 0 }, // Indonesian
{ "uk", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Ukrainian
{ "be", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Belarusian
{ "sl", (n) => n % 100 == 1 ? 1 : n % 100 == 2 ? 2 : n % 100 == 3 || n % 100 == 4 ? 3 : 0 }, // Slovenian
{ "et", (n) => n != 1 ? 1 : 0 }, // Estonian
{ "lv", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n != 0 ? 1 : 2 }, // Latvian
{ "lt", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Lithuanian
{ "fa", (n) => 0 }, // Persian
{ "vi", (n) => 0 }, // Vietnamese
{ "hy", (n) => n != 1 ? 1 : 0 }, // Armenian
{ "eu", (n) => n != 1 ? 1 : 0 }, // Basque
{ "mk", (n) => (n == 1 || n % 10 == 1) ? 1 : (n == 2 || n % 10 == 2) ? 2 : 0 }, // Macedonian
{ "af", (n) => n != 1 ? 1 : 0 }, // Afrikaans
{ "ka", (n) => 0 }, // Georgian
{ "fo", (n) => n != 1 ? 1 : 0 }, // Faroese
{ "hi", (n) => n != 1 ? 1 : 0 }, // Hindi
{ "sw", (n) => n != 1 ? 1 : 0 }, // Swahili
{ "gu", (n) => n != 1 ? 1 : 0 }, // Gujarati
{ "ta", (n) => n != 1 ? 1 : 0 }, // Tamil
{ "te", (n) => n != 1 ? 1 : 0 }, // Telugu
{ "kn", (n) => n != 1 ? 1 : 0 }, // Kannada
{ "mr", (n) => n != 1 ? 1 : 0 }, // Marathi
{ "gl", (n) => n != 1 ? 1 : 0 }, // Gallegan -------------------------------
{ "kok", (n) => n != 1 ? 1 : 0 }, // Konkani -------------------------------
{ "ar-SA", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "bg-BG", (n) => n != 1 ? 1 : 0 }, // Bulgarian
{ "ca-ES", (n) => n != 1 ? 1 : 0 }, // Catalan
{ "zh-TW", (n) => 0 }, // Chinese
{ "cs-CZ", (n) => (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 }, // Czech
{ "da-DK", (n) => n != 1 ? 1 : 0 }, // Danish
{ "de-DE", (n) => n != 1 ? 1 : 0 }, // German
{ "el-GR", (n) => n != 1 ? 1 : 0 }, // Greek
{ "en-US", (n) => n != 1 ? 1 : 0 }, // English
{ "fi-FI", (n) => n != 1 ? 1 : 0 }, // Finnish
{ "fr-FR", (n) => n > 1 ? 1 : 0 }, // French
{ "he-IL", (n) => n != 1 ? 1 : 0 }, // Hebrew
{ "hu-HU", (n) => n != 1 ? 1 : 0 }, // Hungarian
{ "is-IS", (n) => (n % 10 != 1 || n % 100 == 11) ? 1 : 0 }, // Icelandic
{ "it-IT", (n) => n != 1 ? 1 : 0 }, // Italian
{ "ja-JP", (n) => 0 }, // Japanese
{ "ko-KR", (n) => 0 }, // Korean
{ "nl-NL", (n) => n != 1 ? 1 : 0 }, // Dutch
{ "nb-NO", (n) => n != 1 ? 1 : 0 }, // Norwegian
{ "pl-PL", (n) => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Polish
{ "pt-BR", (n) => n > 1 ? 1 : 0 }, // Portuguese
{ "ro-RO", (n) => n == 1 ? 0 : (n == 0 || (n % 100 > 0 && n % 100 < 20)) ? 1 : 2 }, // Romanian
{ "ru-RU", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Russian
{ "hr-HR", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Croatian
{ "sk-SK", (n) => (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2 }, // Slovak
{ "sq-AL", (n) => n != 1 ? 1 : 0 }, // Albanian
{ "sv-SE", (n) => n != 1 ? 1 : 0 }, // Swedish
{ "th-TH", (n) => 0 }, // Thai
{ "tr-TR", (n) => n > 1 ? 1 : 0 }, // Turkish
{ "id-ID", (n) => 0 }, // Indonesian
{ "uk-UA", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Ukrainian
{ "be-BY", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Belarusian
{ "sl-SI", (n) => n % 100 == 1 ? 1 : n % 100 == 2 ? 2 : n % 100 == 3 || n % 100 == 4 ? 3 : 0 }, // Slovenian
{ "et-EE", (n) => n != 1 ? 1 : 0 }, // Estonian
{ "lv-LV", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n != 0 ? 1 : 2 }, // Latvian
{ "lt-LT", (n) => n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 }, // Lithuanian
{ "fa-IR", (n) => 0 }, // Persian
{ "vi-VN", (n) => 0 }, // Vietnamese
{ "hy-AM", (n) => n != 1 ? 1 : 0 }, // Armenian
{ "eu-ES", (n) => n != 1 ? 1 : 0 }, // Basque
{ "mk-MK", (n) => (n == 1 || n % 10 == 1) ? 1 : (n == 2 || n % 10 == 2) ? 2 : 0}, // Macedonian
{ "af-ZA", (n) => n != 1 ? 1 : 0 }, // Afrikaans
{ "ka-GE", (n) => 0 }, // Georgian
{ "fo-FO", (n) => n != 1 ? 1 : 0 }, // Faroese
{ "hi-IN", (n) => n != 1 ? 1 : 0 }, // Hindi
{ "sw-KE", (n) => n != 1 ? 1 : 0 }, // Swahili
{ "gu-IN", (n) => n != 1 ? 1 : 0 }, // Gujarati
{ "ta-IN", (n) => n != 1 ? 1 : 0 }, // Tamil
{ "te-IN", (n) => n != 1 ? 1 : 0 }, // Telugu
{ "kn-IN", (n) => n != 1 ? 1 : 0 }, // Kannada
{ "mr-IN", (n) => n != 1 ? 1 : 0 }, // Marathi
{ "gl-ES", (n) => n != 1 ? 1 : 0 }, // Gallegan -------------------------------
{ "kok-IN", (n) => n != 1 ? 1 : 0 }, // Konkani -------------------------------
{ "ar-IQ", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "zh-CN", (n) => 0 }, // Chinese
{ "de-CH", (n) => n != 1 ? 1 : 0 }, // German
{ "en-GB", (n) => n != 1 ? 1 : 0 }, // English
{ "es-MX", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "fr-BE", (n) => n > 1 ? 1 : 0 }, // French
{ "it-CH", (n) => n != 1 ? 1 : 0 }, // Italian
{ "nl-BE", (n) => n != 1 ? 1 : 0 }, // Dutch
{ "nn-NO", (n) => n != 1 ? 1 : 0 }, // Norwegian
{ "pt-PT", (n) => n != 1 ? 1 : 0 }, // Portuguese
{ "sv-FI", (n) => n != 1 ? 1 : 0 }, // Swedish
{ "ar-EG", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "zh-HK", (n) => 0 }, // Chinese
{ "de-AT", (n) => n != 1 ? 1 : 0 }, // German
{ "en-AU", (n) => n != 1 ? 1 : 0 }, // English
{ "es-ES", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "fr-CA", (n) => n > 1 ? 1 : 0 }, // French
{ "ar-LY", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "zh-SG", (n) => 0 }, // Chinese
{ "de-LU", (n) => n != 1 ? 1 : 0 }, // German
{ "en-CA", (n) => n != 1 ? 1 : 0 }, // English
{ "es-GT", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "fr-CH", (n) => n > 1 ? 1 : 0 }, // French
{ "ar-DZ", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "zh-MO", (n) => 0 }, // Chinese
{ "en-NZ", (n) => n != 1 ? 1 : 0 }, // English
{ "es-CR", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "fr-LU", (n) => n > 1 ? 1 : 0 }, // French
{ "ar-MA", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "en-IE", (n) => n != 1 ? 1 : 0 }, // English
{ "es-PA", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-TN", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "en-ZA", (n) => n != 1 ? 1 : 0 }, // English
{ "es-DO", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-OM", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "es-VE", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-YE", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "es-CO", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-SY", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "es-PE", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-JO", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "en-TT", (n) => n != 1 ? 1 : 0 }, // English
{ "es-AR", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-LB", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "en-ZW", (n) => n != 1 ? 1 : 0 }, // English
{ "es-EC", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-KW", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "en-PH", (n) => n != 1 ? 1 : 0 }, // English
{ "es-CL", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-AE", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "es-UY", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-BH", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "es-PY", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "ar-QA", (n) => n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5 }, // Arabic
{ "es-BO", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "es-SV", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "es-HN", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "es-NI", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "es-PR", (n) => n != 1 ? 1 : 0 }, // Spanish
{ "zh-CHT", (n) => 0 }, // Chinese
{ "ms", (n) => 0 } // Malaysia
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment