Files
stk-code_catmod/src/utils/translation.cpp
2019-06-15 14:11:14 +08:00

697 lines
22 KiB
C++

//
// SuperTuxKart - a fun racing game with go-kart
// Copyright (C) 2006,-2015 2007, 2008 Joerg Henrichs
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 3
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
// Note: the irrlicht include is only here (and esp. before including
// translation.hpp, which contradicts our style rule) to avoid the
// warning message " 'swprintf' : macro redefinition"
// This happens if libintl.h is included before irrlicht.h (since
// both files redefine swprintf).
#include "utils/translation.hpp"
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <fstream>
#include <iostream>
#include <thread>
#if ENABLE_BIDI
# include <fribidi/fribidi.h>
#endif
#include "config/user_config.hpp"
#include "io/file_manager.hpp"
#include "utils/constants.hpp"
#include "utils/log.hpp"
#include "utils/string_utils.hpp"
#ifdef ANDROID
#include "main_android.hpp"
#endif
// set to 1 to debug i18n
#define TRANSLATE_VERBOSE 0
// Define TEST_BIDI to force right-to-left style for all languages
//#define TEST_BIDI
Translations* translations = NULL;
#ifdef LINUX // m_debug
#define PACKAGE "supertuxkart"
#endif
#ifndef SERVER_ONLY
std::map<std::string, std::string> Translations::m_localized_name;
std::map<std::string, std::map<std::string, irr::core::stringw> >
Translations::m_localized_country_codes;
const bool REMOVE_BOM = false;
using namespace tinygettext;
/** The list of available languages; this is global so that it is cached (and remains
even if the translations object is deleted and re-created) */
typedef std::vector<std::string> LanguageList;
static LanguageList g_language_list;
// Note : this method is not static because 'g_language_list' is initialized
// the first time Translations is constructed (despite being a global)
const LanguageList* Translations::getLanguageList() const
{
return &g_language_list;
}
#endif
// ----------------------------------------------------------------------------
/** Frees the memory allocated for the result of toFribidiChar(). */
#ifdef ENABLE_BIDI
void freeFribidiChar(FriBidiChar *str)
{
#ifdef TEST_BIDI
delete[] str;
#else
if (sizeof(wchar_t) != sizeof(FriBidiChar))
delete[] str;
#endif
}
#endif
/** Frees the memory allocated for the result of fromFribidiChar(). */
#ifdef ENABLE_BIDI
void freeFribidiChar(wchar_t *str)
{
if (sizeof(wchar_t) != sizeof(FriBidiChar))
delete[] str;
}
#endif
// ----------------------------------------------------------------------------
/** Converts a wstring to a FriBidi-string.
The caller must take care to free (or not to free) the result after use.
Freeing should be done with freeFribidiChar().
On linux, the string doesn't need to be converted because wchar_t is
already UTF-32. On windows the string is converted from UTF-16 by this
function. */
#ifdef ENABLE_BIDI
FriBidiChar* toFribidiChar(const wchar_t* str)
{
std::size_t length = wcslen(str);
FriBidiChar *result;
if (sizeof(wchar_t) == sizeof(FriBidiChar))
result = (FriBidiChar*) str;
else
{
// On windows FriBidiChar is 4 bytes, but wchar_t is 2 bytes.
// So we simply copy the characters over here (note that this
// is technically incorrect, all characters we use/support fit
// in 16 bits, which is what irrlicht supports atm).
result = new FriBidiChar[length + 1];
for (std::size_t i = 0; i <= length; i++)
result[i] = str[i];
}
#ifdef TEST_BIDI
// Prepend a character in each line that forces RTL style
int lines = 1;
for (std::size_t i = 0; i <= length; i++)
{
if (str[i] == L'\n')
lines++;
}
FriBidiChar *tmp = result;
length += lines;
result = new FriBidiChar[length + 1];
lines = 1;
result[0] = L'\u202E';
for (std::size_t i = 1; i <= length; i++)
{
result[i] = tmp[i - lines];
if (str[i - lines] == L'\n')
{
lines++;
i++;
result[i] = L'\u202E';
}
}
if (sizeof(wchar_t) != sizeof(FriBidiChar))
delete[] tmp;
#endif
return result;
}
wchar_t* fromFribidiChar(const FriBidiChar* str)
{
wchar_t *result;
if (sizeof(wchar_t) == sizeof(FriBidiChar))
result = (wchar_t*) str;
else
{
std::size_t length = 0;
while (str[length])
length++;
// Copy back to wchar_t array
result = new wchar_t[length + 1];
for (std::size_t i = 0; i <= length; i++)
result[i] = str[i];
}
return result;
}
#endif
// ----------------------------------------------------------------------------
Translations::Translations() //: m_dictionary_manager("UTF-16")
{
#ifndef SERVER_ONLY
m_dictionary_manager.add_directory(
file_manager->getAsset(FileManager::TRANSLATION,""));
if (g_language_list.size() == 0)
{
std::set<Language> languages = m_dictionary_manager.get_languages();
// English is always there but may be not found on file system
g_language_list.push_back("en");
for (const Language& language : languages)
{
if (language.str() == "en")
continue;
g_language_list.push_back(language.str());
}
}
if (m_localized_name.empty())
{
const std::string file_name = file_manager->getAsset("localized_name.txt");
try
{
std::unique_ptr<std::istream> in(new std::ifstream(file_name.c_str()));
if (!in.get())
{
Log::error("translation", "error: failure opening: '%s'.",
file_name.c_str());
}
else
{
for (std::string line; std::getline(*in, line, ';'); )
{
line = StringUtils::removeWhitespaces(line);
if (line.empty())
continue;
std::size_t pos = line.find("=");
if (pos == std::string::npos)
continue;
std::string name = line.substr(0, pos);
std::string localized_name = line.substr(pos + 1);
if (name.empty() || localized_name.empty())
continue;
if (localized_name == "0")
{
localized_name =
tinygettext::Language::from_name(name).get_name();
}
m_localized_name[name] = localized_name;
}
}
}
catch(std::exception& e)
{
Log::error("translation", "error: failure extract localized name.");
Log::error("translation", "%s", e.what());
}
}
if (m_localized_country_codes.empty())
{
const std::string file_name = file_manager->getAsset("country_names.csv");
try
{
std::unique_ptr<std::istream> in(new std::ifstream(file_name.c_str()));
if (!in.get())
{
Log::error("translation", "error: failure opening: '%s'.",
file_name.c_str());
}
else
{
std::vector<std::string> header;
std::string line;
while (!StringUtils::safeGetline(*in, line).eof())
{
std::vector<std::string> lists = StringUtils::split(line, ';');
if (lists.size() < 2)
{
Log::error("translation", "Invaild list.");
break;
}
if (lists[0] == "country_code")
{
header = lists;
continue;
}
if (lists.size() != header.size())
{
Log::error("translation", "Different column size.");
break;
}
if (m_localized_country_codes.find(lists[0]) ==
m_localized_country_codes.end())
{
m_localized_country_codes[lists[0]] =
std::map<std::string, irr::core::stringw>();
}
for (unsigned i = 1; i < lists.size(); i++)
{
auto& ret = m_localized_country_codes.at(lists[0]);
ret[header[i]] = StringUtils::utf8ToWide(lists[i]);
}
}
}
}
catch (std::exception& e)
{
Log::error("translation", "error: failure extract localized country name.");
Log::error("translation", "%s", e.what());
}
}
// LC_ALL does not work, sscanf will then not always be able
// to scan for example: s=-1.1,-2.3,-3.3 correctly, which is
// used in driveline files.
#if defined(WIN32) && !defined(__CYGWIN__)
// Windows does not have LC_MESSAGES
setlocale(LC_CTYPE, "");
#else
setlocale(LC_MESSAGES, "");
#endif
/*
bindtextdomain (PACKAGE, file_manager->getTranslationDir().c_str());
if (sizeof(wchar_t) == 4)
{
if (IS_LITTLE_ENDIAN) bind_textdomain_codeset(PACKAGE, "UTF-32LE");
else bind_textdomain_codeset(PACKAGE, "UTF-32BE");
}
else if (sizeof(wchar_t) == 2)
{
bind_textdomain_codeset(PACKAGE, "UTF-16LE");
}
else
{
fprintf(stderr, "Your wchar_t is neither 2 byte-long nor 4. What now??\n");
exit(1);
}
textdomain (PACKAGE);
*/
/*
const std::set<Language>& languages = m_dictionary_manager.get_languages();
Log::info("Translatings", "Number of languages: %d", languages.size());
for (std::set<Language>::const_iterator i = languages.begin();
i != languages.end(); ++i)
{
const Language& language = *i;
Log::info("Translatings", "Env: %s", language.str());
Log::info("Translatings", "Name: %s", language.get_name());
Log::info("Translatings", "Language: %s", language.get_language());
Log::info("Translatings", "Country: %s", language.get_country());
Log::info("Translatings", "Modifier: %s", language.get_modifier());
}
*/
const char *p_language = getenv("LANGUAGE");
std::string language;
if(p_language)
{
language=p_language;
}
else
{
const char *p_lang = getenv("LANG");
if(p_lang)
language = p_lang;
else
{
#ifdef WIN32
// Thanks to the frogatto developer for this code snippet:
char c[1024];
GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO639LANGNAME,
c, 1024);
Log::verbose("translation", "GetLocaleInfo langname returns '%s'.",
c);
if(c[0])
{
language = c;
GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO3166CTRYNAME,
c, 1024);
Log::verbose("translation",
"GetLocaleInfo tryname returns '%s'.", c);
if(c[0]) language += std::string("_")+c;
} // if c[0]
#elif defined(ANDROID)
if (global_android_app)
{
char p_language[3] = {};
AConfiguration_getLanguage(global_android_app->config,
p_language);
std::string s_language(p_language);
if (!s_language.empty())
{
language += s_language;
char p_country[3] = {};
AConfiguration_getCountry(global_android_app->config,
p_country);
std::string s_country(p_country);
if (!s_country.empty())
{
language += "_";
language += s_country;
}
}
}
#endif
} // neither LANGUAGE nor LANG defined
}
if (language != "")
{
Log::verbose("translation", "Env var LANGUAGE = '%s'.",
language.c_str());
if (language.find(":") != std::string::npos)
{
std::vector<std::string> langs = StringUtils::split(language, ':');
Language l;
for (unsigned int curr=0; curr<langs.size(); curr++)
{
l = Language::from_env(langs[curr]);
if (l)
{
Log::verbose("translation", "Language '%s'.",
l.get_name().c_str());
m_dictionary = m_dictionary_manager.get_dictionary(l);
break;
}
}
m_current_language_name = l.get_name();
m_current_language_name_code = l.get_language();
m_current_language_tag = m_current_language_name_code;
if (!l.get_country().empty())
{
m_current_language_tag += "-";
m_current_language_tag += l.get_country();
}
if (!l)
{
m_dictionary = m_dictionary_manager.get_dictionary();
}
}
else
{
const Language& tgtLang = Language::from_env(language);
if (!tgtLang)
{
Log::warn("Translation", "Unsupported language '%s'", language.c_str());
UserConfigParams::m_language = "system";
m_current_language_name = "Default language";
m_current_language_name_code = "en";
m_current_language_tag = "en";
m_dictionary = m_dictionary_manager.get_dictionary();
}
else
{
m_current_language_name = tgtLang.get_name();
m_current_language_name_code = tgtLang.get_language();
m_current_language_tag = m_current_language_name_code;
if (!tgtLang.get_country().empty())
{
m_current_language_tag += "-";
m_current_language_tag += tgtLang.get_country();
}
Log::verbose("translation", "Language '%s'.", m_current_language_name.c_str());
m_dictionary = m_dictionary_manager.get_dictionary(tgtLang);
}
}
}
else
{
m_current_language_name = "Default language";
m_current_language_name_code = "en";
m_current_language_tag = m_current_language_name_code;
m_dictionary = m_dictionary_manager.get_dictionary();
}
// This is a silly but working hack I added to determine whether the
// current language is RTL or not, since gettext doesn't seem to provide
// this information
// This one is just for the xgettext parser to pick up
#define ignore(X)
//I18N: Do NOT literally translate this string!! Please enter Y as the
// translation if your language is a RTL (right-to-left) language,
// N (or nothing) otherwise
ignore(_(" Is this a RTL language?"));
const std::string isRtl =
m_dictionary.translate(" Is this a RTL language?");
m_rtl = false;
for (unsigned int n=0; n < isRtl.size(); n++)
{
if (isRtl[n] == 'Y')
{
m_rtl = true;
break;
}
}
#ifdef TEST_BIDI
m_rtl = true;
#endif
#endif
} // Translations
// ----------------------------------------------------------------------------
Translations::~Translations()
{
} // ~Translations
// ----------------------------------------------------------------------------
bool Translations::isRTLText(const wchar_t *in_ptr)
{
#if ENABLE_BIDI
std::size_t length = wcslen(in_ptr);
FriBidiChar *fribidiInput = toFribidiChar(in_ptr);
FriBidiCharType *types = new FriBidiCharType[length];
fribidi_get_bidi_types(fribidiInput, (FriBidiStrIndex)length, types);
freeFribidiChar(fribidiInput);
// Declare as RTL if one character is RTL
for (std::size_t i = 0; i < length; i++)
{
if (types[i] & FRIBIDI_MASK_RTL)
{
delete[] types;
return true;
}
}
delete[] types;
return false;
#else
return false;
#endif
}
/**
* \param original Message to translate
* \param context Optional, can be set to differentiate 2 strings that are identical
* in English but could be different in other languages
*/
const wchar_t* Translations::w_gettext(const wchar_t* original, const char* context)
{
std::string in = StringUtils::wideToUtf8(original);
return w_gettext(in.c_str(), context);
}
/**
* \param original Message to translate
* \param context Optional, can be set to differentiate 2 strings that are identical
* in English but could be different in other languages
*/
const wchar_t* Translations::w_gettext(const char* original, const char* context)
{
#ifdef SERVER_ONLY
static irr::core::stringw dummy_for_server;
dummy_for_server = StringUtils::utf8ToWide(original);
return dummy_for_server.c_str();
#else
if (original[0] == '\0') return L"";
#if TRANSLATE_VERBOSE
Log::info("Translations", "Translating %s", original);
#endif
const std::string& original_t = (context == NULL ?
m_dictionary.translate(original) :
m_dictionary.translate_ctxt(context, original));
// print
//for (int n=0;; n+=4)
std::lock_guard<std::mutex> lock(m_gettext_mutex);
static std::map<std::thread::id, core::stringw> original_tw;
original_tw[std::this_thread::get_id()] = StringUtils::utf8ToWide(original_t);
const wchar_t* out_ptr = original_tw.at(std::this_thread::get_id()).c_str();
if (REMOVE_BOM) out_ptr++;
#if TRANSLATE_VERBOSE
std::wcout << L" translation : " << out_ptr << std::endl;
#endif
return out_ptr;
#endif
}
/**
* \param singular Message to translate in singular form
* \param plural Message to translate in plural form (can be the same as the singular form)
* \param num Count used to obtain the correct plural form.
* \param context Optional, can be set to differentiate 2 strings that are identical
* in English but could be different in other languages
*/
const wchar_t* Translations::w_ngettext(const wchar_t* singular, const wchar_t* plural, int num, const char* context)
{
std::string in = StringUtils::wideToUtf8(singular);
std::string in2 = StringUtils::wideToUtf8(plural);
return w_ngettext(in.c_str(), in2.c_str(), num, context);
}
/**
* \param singular Message to translate in singular form
* \param plural Message to translate in plural form (can be the same as the singular form)
* \param num Count used to obtain the correct plural form.
* \param context Optional, can be set to differentiate 2 strings that are identical
* in English but could be different in other languages
*/
const wchar_t* Translations::w_ngettext(const char* singular, const char* plural, int num, const char* context)
{
#ifdef SERVER_ONLY
static core::stringw str_buffer;
str_buffer = StringUtils::utf8ToWide(singular);
return str_buffer.c_str();
#else
const std::string& res = (context == NULL ?
m_dictionary.translate_plural(singular, plural, num) :
m_dictionary.translate_ctxt_plural(context, singular, plural, num));
std::lock_guard<std::mutex> lock(m_ngettext_mutex);
static std::map<std::thread::id, core::stringw> str_buffer;
str_buffer[std::this_thread::get_id()] = StringUtils::utf8ToWide(res);
const wchar_t* out_ptr = str_buffer.at(std::this_thread::get_id()).c_str();
if (REMOVE_BOM) out_ptr++;
#if TRANSLATE_VERBOSE
std::wcout << L" translation : " << out_ptr << std::endl;
#endif
return out_ptr;
#endif
}
#ifndef SERVER_ONLY
std::set<wchar_t> Translations::getCurrentAllChar()
{
return m_dictionary.get_all_used_chars();
}
std::string Translations::getCurrentLanguageName()
{
return m_current_language_name;
//return m_dictionary_manager.get_language().get_name();
}
std::string Translations::getCurrentLanguageNameCode()
{
return m_current_language_name_code;
}
const std::string& Translations::getLocalizedName(const std::string& str) const
{
std::map<std::string, std::string>::const_iterator n = m_localized_name.find(str);
assert (n != m_localized_name.end());
return n->second;
}
/* Convert 2-letter country code to localized readable name.
*/
irr::core::stringw Translations::getLocalizedCountryName(const std::string& country_code) const
{
auto it = m_localized_country_codes.find(country_code);
// If unknown 2 letter country just return the same
if (it == m_localized_country_codes.end())
return StringUtils::utf8ToWide(country_code);
auto name_itr = it->second.find(m_current_language_tag);
if (name_itr != it->second.end())
return name_itr->second;
// If there should be invalid language tag, use en (which always exists)
name_itr = it->second.find("en");
if (name_itr != it->second.end())
return name_itr->second;
// Fallback
return StringUtils::utf8ToWide(country_code);
}
#endif