diff --git a/build/premake/extern_libs4.lua b/build/premake/extern_libs4.lua index ec57c181cf..75e9af40be 100644 --- a/build/premake/extern_libs4.lua +++ b/build/premake/extern_libs4.lua @@ -16,6 +16,7 @@ else end -- directory for shared, bundled libraries libraries_source_dir = rootdir.."/libraries/source/" +third_party_source_dir = rootdir.."/source/third_party/" local function add_default_lib_paths(extern_lib) libdirs { libraries_dir .. extern_lib .. "/lib" } @@ -33,6 +34,10 @@ local function add_source_include_paths(extern_lib) includedirs { libraries_source_dir .. extern_lib .. "/include" } end +local function add_third_party_include_paths(extern_lib) + includedirs { third_party_source_dir .. extern_lib .. "/include" } +end + -- For unixes: add buildflags and linkflags using pkg-config or another similar command. -- By default, pkg-config is used. Other commands can be passed as "alternative_cmd". @@ -359,6 +364,58 @@ extern_lib_defs = { end end, }, + iconv = { + compile_settings = function() + if os.is("windows") then + add_default_include_paths("iconv") + defines { "HAVE_ICONV_CONST" } + defines { "LIBICONV_STATIC" } + end + end, + link_settings = function() + if os.is("windows") then + add_default_lib_paths("iconv") + end + add_default_links({ + win_names = { "iconv" }, + -- TODO: glibc provides symbols for this, so we should only include that (and depend on libiconv) on non-glibc unix + --unix_names = { "iconv" }, + dbg_suffix = "", + }) + end, + }, + icu = { + compile_settings = function() + if os.is("windows") then + add_default_include_paths("icu") + elseif os.is("macosx") then + -- Support ICU_CONFIG for overriding the default PATH-based icu-config + icu_config_path = os.getenv("ICU_CONFIG") + if not icu_config_path then + icu_config_path = "icu-config" + end + pkgconfig_cflags(nil, icu_config_path.." --cppflags") + end + end, + link_settings = function() + if os.is("windows") then + add_default_lib_paths("icu") + end + if os.is("macosx") then + icu_config_path = os.getenv("ICU_CONFIG") + if not icu_config_path then + icu_config_path = "gloox-config" + end + pkgconfig_libs(nil, icu_config_path.." --ldflags-searchpath --ldflags-libsonly --ldflags-system") + else + add_default_links({ + win_names = { "icuuc", "icuin" }, + unix_names = { "icui18n", "icuuc" }, + dbg_suffix = "", + }) + end + end, + }, libcurl = { compile_settings = function() if os.is("windows") or os.is("macosx") then @@ -603,6 +660,11 @@ extern_lib_defs = { end end, }, + tinygettext = { + compile_settings = function() + add_third_party_include_paths("tinygettext") + end, + }, valgrind = { compile_settings = function() add_source_include_paths("valgrind") diff --git a/build/premake/premake4.lua b/build/premake/premake4.lua index 69deeaeed3..16b7210a35 100644 --- a/build/premake/premake4.lua +++ b/build/premake/premake4.lua @@ -542,6 +542,12 @@ function setup_static_lib_project (project_name, rel_source_dirs, extern_libs, e end end +function setup_third_party_static_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) + + setup_static_lib_project(project_name, rel_source_dirs, extern_libs, extra_params) + includedirs { source_root .. "third_party/" .. project_name .. "/include/" .. project_name } +end + function setup_shared_lib_project (project_name, rel_source_dirs, extern_libs, extra_params) local target_type = "SharedLib" @@ -586,6 +592,27 @@ function setup_all_libs () end setup_static_lib_project("network", source_dirs, extern_libs, {}) + source_dirs = { + "third_party/tinygettext/src", + } + extern_libs = { + "iconv", + } + setup_third_party_static_lib_project("tinygettext", source_dirs, extern_libs, { no_pch = 1 }) + + -- it's an external library and we don't want to modify its source to fix warnings, so we just disable them to avoid noise in the compile output + if _ACTION == "vs2005" or _ACTION == "vs2008" or _ACTION == "vs2010" or _ACTION == "vs2012" or _ACTION == "vs2013" then + buildoptions { + "/wd4127", + "/wd4309", + "/wd4800", + "/wd4100", + "/wd4996", + "/wd4099", + "/wd4503" + } + end + if not _OPTIONS["without-lobby"] then source_dirs = { @@ -673,6 +700,8 @@ function setup_all_libs () "soundmanager/scripting", "maths", "maths/scripting", + "i18n", + "i18n/scripting" } extern_libs = { "spidermonkey", @@ -683,6 +712,9 @@ function setup_all_libs () "boost", "enet", "libcurl", + "tinygettext", + "icu", + "iconv", } if not _OPTIONS["without-audio"] then @@ -727,13 +759,17 @@ function setup_all_libs () source_dirs = { "gui", - "gui/scripting" + "gui/scripting", + "i18n" } extern_libs = { "spidermonkey", "sdl", -- key definitions "opengl", "boost", + "tinygettext", + "icu", + "iconv", } if not _OPTIONS["without-audio"] then table.insert(extern_libs, "openal") @@ -858,6 +894,9 @@ used_extern_libs = { "comsuppw", "enet", "libcurl", + "tinygettext", + "icu", + "iconv", "valgrind", } diff --git a/source/gui/CGUI.cpp b/source/gui/CGUI.cpp index 67f0a792c8..f2dfc524a2 100644 --- a/source/gui/CGUI.cpp +++ b/source/gui/CGUI.cpp @@ -47,8 +47,10 @@ CGUI #include "graphics/TextRenderer.h" #include "lib/input.h" #include "lib/bits.h" +#include "i18n/L10n.h" #include "lib/timer.h" #include "lib/sysdep/sysdep.h" +#include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Hotkey.h" @@ -1095,6 +1097,10 @@ void CGUI::Xeromyces_ReadObject(XMBElement Element, CXeromyces* pFile, IGUIObjec ELMT(object); ELMT(action); ELMT(repeat); + ELMT(translatableAttribute); + ELMT(translate); + ELMT(attribute); + ELMT(keep); ATTR(style); ATTR(type); ATTR(name); @@ -1102,6 +1108,7 @@ void CGUI::Xeromyces_ReadObject(XMBElement Element, CXeromyces* pFile, IGUIObjec ATTR(z); ATTR(on); ATTR(file); + ATTR(id); // // Read Style and set defaults @@ -1242,8 +1249,27 @@ void CGUI::Xeromyces_ReadObject(XMBElement Element, CXeromyces* pFile, IGUIObjec code = scriptfile.DecodeUTF8(); // assume it's UTF-8 } - // Read the inline code (concatenating to the file code, if both are specified) - code += CStr(child.GetText()); + XMBElementList grandchildren = child.GetChildNodes(); + if (grandchildren.Count > 0) // The element contains and tags. + { + for (int i = 0; i < grandchildren.Count; ++i) + { + XMBElement grandchild = grandchildren.Item(i); + if (grandchild.GetNodeName() == elmt_translate) + { + code += L10n::Instance().Translate(grandchild.GetText()); + } + else if (grandchild.GetNodeName() == elmt_keep) + { + code += grandchild.GetText(); + } + } + } + else // It’s pure JavaScript code. + { + // Read the inline code (concatenating to the file code, if both are specified) + code += CStr(child.GetText()); + } CStr action = CStr(child.GetAttributes().GetNamedItem(attr_on)); @@ -1255,6 +1281,53 @@ void CGUI::Xeromyces_ReadObject(XMBElement Element, CXeromyces* pFile, IGUIObjec { Xeromyces_ReadRepeat(child, pFile, object, Paths); } + else if (element_name == elmt_translatableAttribute) + { + // This is an element in the form “attributeValue”. + CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name. + if (!attributeName.empty()) + { + CStr value(child.GetText()); + if (!value.empty()) + { + CStr translatedValue(L10n::Instance().Translate(value)); + object->SetSetting(attributeName, translatedValue.UnescapeBackslashes().FromUTF8(), true); + } + } + else // Ignore. + { + LOGERROR(L"GUI: ‘attribute’ XML element with empty ‘id’ XML attribute found. (object: %hs)", object->GetPresentableName().c_str()); + } + } + else if (element_name == elmt_attribute) + { + // This is an element in the form “Don’t translate this part + // but translate this one.”. + CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name. + if (!attributeName.empty()) + { + CStr translatedValue; + + XMBElementList grandchildren = child.GetChildNodes(); + for (int i = 0; i < grandchildren.Count; ++i) + { + XMBElement grandchild = grandchildren.Item(i); + if (grandchild.GetNodeName() == elmt_translate) + { + translatedValue += L10n::Instance().Translate(grandchild.GetText()); + } + else if (grandchild.GetNodeName() == elmt_keep) + { + translatedValue += grandchild.GetText(); + } + } + object->SetSetting(attributeName, translatedValue.UnescapeBackslashes().FromUTF8(), true); + } + else // Ignore. + { + LOGERROR(L"GUI: ‘attribute’ XML element with empty ‘id’ XML attribute found. (object: %hs)", object->GetPresentableName().c_str()); + } + } else { // Try making the object read the tag. diff --git a/source/gui/COList.cpp b/source/gui/COList.cpp index 21abb554d2..01b87c52ff 100644 --- a/source/gui/COList.cpp +++ b/source/gui/COList.cpp @@ -1,5 +1,6 @@ #include "precompiled.h" #include "COList.h" +#include "i18n/L10n.h" #include "ps/CLogger.h" @@ -121,9 +122,13 @@ void COList::HandleMessage(SGUIMessage &Message) bool COList::HandleAdditionalChildren(const XMBElement& child, CXeromyces* pFile) { - int elmt_item = pFile->GetElementID("item"); - int elmt_heading = pFile->GetElementID("heading"); - int elmt_def = pFile->GetElementID("def"); + #define ELMT(x) int elmt_##x = pFile->GetElementID(#x) + #define ATTR(x) int attr_##x = pFile->GetAttributeID(#x) + ELMT(item); + ELMT(heading); + ELMT(def); + ELMT(translatableAttribute); + ATTR(id); if (child.GetNodeName() == elmt_item) { @@ -180,6 +185,30 @@ bool COList::HandleAdditionalChildren(const XMBElement& child, CXeromyces* pFile } + XMBElementList grandchildren = child.GetChildNodes(); + for (int i = 0; i < grandchildren.Count; ++i) + { + XMBElement grandchild = grandchildren.Item(i); + if (grandchild.GetNodeName() == elmt_translatableAttribute) + { + CStr attributeName(grandchild.GetAttributes().GetNamedItem(attr_id)); + // only the heading is translatable for list defs + if (!attributeName.empty() && attributeName == "heading") + { + CStr value(grandchild.GetText()); + if (!value.empty()) + { + CStr translatedValue(L10n::Instance().Translate(value)); + oDef.m_Heading = translatedValue.FromUTF8(); + } + } + else // Ignore. + { + LOGERROR(L"GUI: translatable attribute in olist def that isn't a heading. (object: %hs)", this->GetPresentableName().c_str()); + } + } + } + m_ObjectsDefs.push_back(oDef); AddSetting(GUIST_CGUIList, oDef.m_Id); diff --git a/source/gui/GUIRenderer.cpp b/source/gui/GUIRenderer.cpp index 26ccdc8227..5e39ebf1e6 100644 --- a/source/gui/GUIRenderer.cpp +++ b/source/gui/GUIRenderer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2014 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,6 +22,7 @@ #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "gui/GUIutil.h" +#include "i18n/L10n.h" #include "lib/ogl.h" #include "lib/utf8.h" #include "lib/res/h_mgr.h" @@ -200,7 +201,7 @@ void GUIRenderer::UpdateDrawCallCache(DrawCalls &Calls, const CStr& SpriteName, if (!(*cit)->m_TextureName.empty()) { - CTextureProperties textureProps((*cit)->m_TextureName); + CTextureProperties textureProps(L10n::Instance().LocalizePath((*cit)->m_TextureName)); textureProps.SetWrap((*cit)->m_WrapMode); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); diff --git a/source/gui/GUItext.cpp b/source/gui/GUItext.cpp index fcb6adf6de..6be57f755a 100644 --- a/source/gui/GUItext.cpp +++ b/source/gui/GUItext.cpp @@ -52,6 +52,36 @@ void CGUIString::SFeedback::Reset() m_NewLine=false; } +// TODO this method should be removed at one point +// It is a solution for CParser not recognising backslash-escaped quoted +CStrW UnescapeString(CStrW originalString) +{ + // no reason to mess with more memory if there is nothing to unescape + if (originalString.Find('&') == -1) + return originalString; + // clone the string + CStrW newString = CStrW(); + for (ulong i; i < originalString.length(); ++i) + { + if (originalString[i] == '&' && originalString[i+1] == '#') + { + i += 2; + wchar_t c = 0; + while (originalString[i] >= '0' && originalString[i] <= '9') + { + c = c*10 + (originalString[i] - '0'); + ++i; + } + ENSURE(originalString[i] == ';'); + if (c >= ' ') + newString += c; + } + else + newString += originalString[i]; + } + return newString; +} + void CGUIString::GenerateTextCall(const CGUI *pGUI, SFeedback &Feedback, CStrIntern DefaultFont, @@ -213,7 +243,7 @@ void CGUIString::GenerateTextCall(const CGUI *pGUI, TextCall.m_UseCustomColor = false; // Extract substd::string from RawString. - TextCall.m_String = GetRawString().substr(_from, _to-_from); + TextCall.m_String = UnescapeString(GetRawString().substr(_from, _to-_from)); // Go through tags and apply changes. std::vector::const_iterator it2; diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp index a8e2942129..b6452d386a 100644 --- a/source/gui/scripting/ScriptFunctions.cpp +++ b/source/gui/scripting/ScriptFunctions.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2014 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -27,10 +27,12 @@ #include "gui/IGUIObject.h" #include "gui/scripting/JSInterface_GUITypes.h" #include "graphics/scripting/JSInterface_GameView.h" -#include "lib/timer.h" -#include "lib/utf8.h" +#include "i18n/L10n.h" +#include "i18n/scripting/JSInterface_L10n.h" #include "lib/svn_revision.h" #include "lib/sysdep/sysdep.h" +#include "lib/timer.h" +#include "lib/utf8.h" #include "lobby/scripting/JSInterface_Lobby.h" #include "maths/FixedVector3D.h" #include "network/NetClient.h" @@ -54,7 +56,6 @@ #include "ps/UserReport.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" -#include "ps/ConfigDB.h" #include "renderer/scripting/JSInterface_Renderer.h" #include "tools/atlas/GameInterface/GameLoop.h" @@ -696,28 +697,53 @@ CScriptVal GetGUIObjectByName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CS // lib/svn_revision.cpp. it is useful to know when attempting to // reproduce bugs (the main EXE and PDB should be temporarily reverted to // that revision so that they match user-submitted crashdumps). -CStr GetBuildTimestamp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int mode) +std::wstring GetBuildTimestamp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int mode) { char buf[200]; - - // see function documentation - switch(mode) + if (mode == -1) // Date, time and revision. { - case -1: - sprintf_s(buf, ARRAY_SIZE(buf), "%s %s (%ls)", __DATE__, __TIME__, svn_revision); - break; - case 0: - sprintf_s(buf, ARRAY_SIZE(buf), "%s", __DATE__); - break; - case 1: - sprintf_s(buf, ARRAY_SIZE(buf), "%s", __TIME__); - break; - case 2: - sprintf_s(buf, ARRAY_SIZE(buf), "%ls", svn_revision); - break; + UDate dateTime = L10n::Instance().ParseDateTime(__DATE__ " " __TIME__, "MMM d yyyy HH:mm:ss", Locale::getUS()); + std::string dateTimeString = L10n::Instance().LocalizeDateTime(dateTime, L10n::DateTime, SimpleDateFormat::DATE_TIME); + char svnRevision[32]; + sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision); + if (strcmp(svnRevision, "custom build") == 0) + { + // Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build. + sprintf_s(buf, ARRAY_SIZE(buf), L10n::Instance().Translate("%s (custom build)").c_str(), dateTimeString.c_str()); + } + else + { + // Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build. + sprintf_s(buf, ARRAY_SIZE(buf), L10n::Instance().Translate("%s (%ls)").c_str(), dateTimeString.c_str(), svn_revision); + } + } + else if (mode == 0) // Date. + { + UDate dateTime = L10n::Instance().ParseDateTime(__DATE__, "MMM d yyyy", Locale::getUS()); + std::string dateTimeString = L10n::Instance().LocalizeDateTime(dateTime, L10n::Date, SimpleDateFormat::MEDIUM); + sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str()); + } + else if (mode == 1) // Time. + { + UDate dateTime = L10n::Instance().ParseDateTime(__TIME__, "HH:mm:ss", Locale::getUS()); + std::string dateTimeString = L10n::Instance().LocalizeDateTime(dateTime, L10n::Time, SimpleDateFormat::MEDIUM); + sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str()); + } + else if (mode == 2) // Revision. + { + char svnRevision[32]; + sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision); + if (strcmp(svnRevision, "custom build") == 0) + { + sprintf_s(buf, ARRAY_SIZE(buf), L10n::Instance().Translate("custom build").c_str()); + } + else + { + sprintf_s(buf, ARRAY_SIZE(buf), "%ls", svn_revision); + } } - return CStr(buf); + return wstring_from_utf8(buf); } //----------------------------------------------------------------------------- @@ -773,7 +799,6 @@ void StartJsTimer(ScriptInterface::CxPrivate* pCxPrivate, unsigned int slot) js_start_times[slot].SetFromTimer(); } - void StopJsTimer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int slot) { if (slot >= MAX_JS_TIMERS) @@ -785,9 +810,6 @@ void StopJsTimer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int sl BillingPolicy_Default()(&js_timer_clients[slot], js_start_times[slot], now); js_start_times[slot].SetToZero(); } - - - } // namespace @@ -796,12 +818,13 @@ void GuiScriptingInit(ScriptInterface& scriptInterface) { JSI_IGUIObject::init(scriptInterface); JSI_GUITypes::init(scriptInterface); - + JSI_GameView::RegisterScriptFunctions(scriptInterface); JSI_Renderer::RegisterScriptFunctions(scriptInterface); JSI_Console::RegisterScriptFunctions(scriptInterface); JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); JSI_Sound::RegisterScriptFunctions(scriptInterface); + JSI_L10n::RegisterScriptFunctions(scriptInterface); // VFS (external) scriptInterface.RegisterFunction("BuildDirEntList"); @@ -876,7 +899,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface) scriptInterface.RegisterFunction("IsPaused"); scriptInterface.RegisterFunction("SetPaused"); scriptInterface.RegisterFunction("GetFPS"); - scriptInterface.RegisterFunction("BuildTime"); + scriptInterface.RegisterFunction("BuildTime"); // User report functions scriptInterface.RegisterFunction("IsUserReportEnabled"); diff --git a/source/i18n/L10n.cpp b/source/i18n/L10n.cpp new file mode 100644 index 0000000000..e32250ac4e --- /dev/null +++ b/source/i18n/L10n.cpp @@ -0,0 +1,486 @@ +/* Copyright (c) 2014 Wildfire Games + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "precompiled.h" +#include "i18n/L10n.h" + +#include +#include +#include + +#include "lib/file/file_system.h" +#include "lib/utf8.h" + +#include "ps/CLogger.h" +#include "ps/ConfigDB.h" +#include "ps/Filesystem.h" +#include "ps/GameSetup/GameSetup.h" + + +L10n& L10n::Instance() +{ + static L10n m_instance; + return m_instance; +} + +L10n::L10n() + : currentLocaleIsOriginalGameLocale(false), dictionary(new tinygettext::Dictionary()) +{ + LoadListOfAvailableLocales(); + ReevaluateCurrentLocaleAndReload(); +} + +L10n::~L10n() +{ + for (std::vector::iterator iterator = availableLocales.begin(); iterator != availableLocales.end(); ++iterator) + delete *iterator; + delete dictionary; +} + +Locale L10n::GetCurrentLocale() +{ + return currentLocale; +} + +bool L10n::SaveLocale(const std::string& localeCode) +{ + return SaveLocale(Locale(Locale::createCanonical(localeCode.c_str()))); +} + +bool L10n::SaveLocale(Locale locale) +{ + // TODO: Use the ConfigDB functions exposed to js to change the config value + // Save the new locale in the settings file. + if (!ValidateLocale(locale)) + return false; + + g_ConfigDB.SetValueString(CFG_USER, "locale", locale.getName()); + g_ConfigDB.WriteFile(CFG_USER); + return true; +} + +bool L10n::ValidateLocale(const std::string& localeCode) +{ + return ValidateLocale(Locale::createCanonical(localeCode.c_str())); +} + +// Returns true if both of these conditions are true: +// 1. ICU has resources for that locale (which also ensures it's a valid locale string) +// 2. Either a dictionary for language_country or for language is available. +bool L10n::ValidateLocale(Locale locale) +{ + int32_t count; + bool icuSupported = false; + const Locale* icuSupportedLocales = Locale::getAvailableLocales(count); + for (int i=0; i::iterator iterator = availableLocales.begin(); iterator != availableLocales.end(); ++iterator) + { + if ((strcmp((*iterator)->getLanguage(), locale.getLanguage()) == 0 && strcmp((*iterator)->getCountry(), "") == 0) || + (strcmp((*iterator)->getLanguage(), locale.getLanguage()) == 0 && strcmp((*iterator)->getCountry(), locale.getCountry()) == 0)) + { + return true; + } + } + return false; +} + +std::vector L10n::GetDictionariesForDictLocale(const std::string& locale) +{ + std::wstring tmpLocale(locale.begin(), locale.end()); + std::vector ret; + VfsPaths filenames; + if (vfs::GetPathnames(g_VFS, L"l10n/", tmpLocale.append(L".*.po").c_str(), filenames) < 0) + return ret; + + if (filenames.size() == 0) + { + Locale tmpLocale1 = Locale::createCanonical(locale.c_str()); + if (vfs::GetPathnames(g_VFS, L"l10n/", wstring_from_utf8(tmpLocale1.getLanguage()).append(L".*.po").c_str(), filenames) < 0) + return ret; + } + + for (VfsPaths::iterator it = filenames.begin(); it != filenames.end(); ++it) + { + VfsPath filepath = *it; + ret.push_back(filepath.Filename().string()); + } + return ret; +} + +std::string L10n::GetDictionaryLocale(std::string configLocaleString) +{ + Locale out; + GetDictionaryLocale(configLocaleString, out); + return out.getName(); +} + +// First, try to get a valid locale from the config, then check if the system locale can be used and otherwise fall back to en_US. +void L10n::GetDictionaryLocale(std::string configLocaleString, Locale& outLocale) +{ + bool validConfigLocale = false; + if (!configLocaleString.empty()) + { + Locale configLocale = Locale::createCanonical(configLocaleString.c_str()); + if (ValidateLocale(configLocale)) + { + outLocale = configLocale; + validConfigLocale = true; + } + else + LOGWARNING(L"The configured locale is not valid or no translations are available. Falling back to another locale."); + } + + if (!validConfigLocale) + { + Locale systemLocale = Locale::getDefault(); + if (ValidateLocale(systemLocale)) + { + outLocale = systemLocale; + } + else + { + outLocale = Locale::getUS(); + } + } +} + + +// Try to find the best disctionary locale based on user configuration and system locale, set the currentLocale and reload the dictionary. +void L10n::ReevaluateCurrentLocaleAndReload() +{ + std::string locale; + CFG_GET_VAL("locale", String, locale); + + GetDictionaryLocale(locale, currentLocale); + currentLocaleIsOriginalGameLocale = (currentLocale == Locale::getUS()) == TRUE; + LoadDictionaryForCurrentLocale(); +} + +// Get all locales supported by ICU. +std::vector L10n::GetAllLocales() +{ + std::vector ret; + int32_t count; + const Locale* icuSupportedLocales = Locale::getAvailableLocales(count); + for (int i=0; i L10n::GetSupportedLocaleBaseNames() +{ + std::vector supportedLocaleCodes; + for (std::vector::iterator iterator = availableLocales.begin(); iterator != availableLocales.end(); ++iterator) + { + if (!InDevelopmentCopy() && strcmp((*iterator)->getBaseName(), "long") == 0) + continue; + supportedLocaleCodes.push_back((*iterator)->getBaseName()); + } + return supportedLocaleCodes; +} + +std::vector L10n::GetSupportedLocaleDisplayNames() +{ + std::vector supportedLocaleDisplayNames; + for (std::vector::iterator iterator = availableLocales.begin(); iterator != availableLocales.end(); ++iterator) + { + if (strcmp((*iterator)->getBaseName(), "long") == 0) + { + if (InDevelopmentCopy()) + supportedLocaleDisplayNames.push_back(wstring_from_utf8(Translate("Long strings"))); + continue; + } + + UnicodeString utf16LocaleDisplayName; + (**iterator).getDisplayName(**iterator, utf16LocaleDisplayName); + char localeDisplayName[512]; + CheckedArrayByteSink sink(localeDisplayName, ARRAY_SIZE(localeDisplayName)); + utf16LocaleDisplayName.toUTF8(sink); + ENSURE(!sink.Overflowed()); + + supportedLocaleDisplayNames.push_back(wstring_from_utf8(std::string(localeDisplayName, sink.NumberOfBytesWritten()))); + } + return supportedLocaleDisplayNames; +} + +std::string L10n::GetCurrentLocaleString() +{ + return currentLocale.getName(); +} + +std::string L10n::GetLocaleLanguage(const std::string& locale) +{ + Locale loc = Locale::createCanonical(locale.c_str()); + return loc.getLanguage(); +} + +std::string L10n::GetLocaleBaseName(const std::string& locale) +{ + Locale loc = Locale::createCanonical(locale.c_str()); + return loc.getBaseName(); +} + +std::string L10n::GetLocaleCountry(const std::string& locale) +{ + Locale loc = Locale::createCanonical(locale.c_str()); + return loc.getCountry(); +} + +std::string L10n::GetLocaleScript(const std::string& locale) +{ + Locale loc = Locale::createCanonical(locale.c_str()); + return loc.getScript(); +} + +std::string L10n::Translate(const std::string& sourceString) +{ + if (!currentLocaleIsOriginalGameLocale) + return dictionary->translate(sourceString); + + return sourceString; +} + +std::string L10n::TranslateWithContext(const std::string& context, const std::string& sourceString) +{ + if (!currentLocaleIsOriginalGameLocale) + return dictionary->translate_ctxt(context, sourceString); + + return sourceString; +} + +std::string L10n::TranslatePlural(const std::string& singularSourceString, const std::string& pluralSourceString, int number) +{ + if (!currentLocaleIsOriginalGameLocale) + return dictionary->translate_plural(singularSourceString, pluralSourceString, number); + + if (number == 1) + return singularSourceString; + + return pluralSourceString; +} + +std::string L10n::TranslatePluralWithContext(const std::string& context, const std::string& singularSourceString, const std::string& pluralSourceString, int number) +{ + if (!currentLocaleIsOriginalGameLocale) + return dictionary->translate_ctxt_plural(context, singularSourceString, pluralSourceString, number); + + if (number == 1) + return singularSourceString; + + return pluralSourceString; +} + +std::string L10n::TranslateLines(const std::string& sourceString) +{ + std::string targetString; + std::stringstream stringOfLines(sourceString); + std::string line; + + while (std::getline(stringOfLines, line)) { + targetString.append(Translate(line)); + targetString.append("\n"); + } + + return targetString; +} + +UDate L10n::ParseDateTime(const std::string& dateTimeString, const std::string& dateTimeFormat, const Locale& locale) +{ + UErrorCode success = U_ZERO_ERROR; + UnicodeString utf16DateTimeString = UnicodeString::fromUTF8(dateTimeString.c_str()); + UnicodeString utf16DateTimeFormat = UnicodeString::fromUTF8(dateTimeFormat.c_str()); + + DateFormat* dateFormatter = new SimpleDateFormat(utf16DateTimeFormat, locale, success); + UDate date = dateFormatter->parse(utf16DateTimeString, success); + delete dateFormatter; + + return date; +} + +std::string L10n::LocalizeDateTime(const UDate& dateTime, DateTimeType type, DateFormat::EStyle style) +{ + UnicodeString utf16Date; + + DateFormat* dateFormatter = CreateDateTimeInstance(type, style, currentLocale); + dateFormatter->format(dateTime, utf16Date); + char utf8Date[512]; + CheckedArrayByteSink sink(utf8Date, ARRAY_SIZE(utf8Date)); + utf16Date.toUTF8(sink); + ENSURE(!sink.Overflowed()); + delete dateFormatter; + + return std::string(utf8Date, sink.NumberOfBytesWritten()); +} + +std::string L10n::FormatMillisecondsIntoDateString(UDate milliseconds, const std::string& formatString) +{ + UErrorCode success = U_ZERO_ERROR; + UnicodeString utf16Date; + UnicodeString utf16LocalizedDateTimeFormat = UnicodeString::fromUTF8(formatString.c_str()); + + // The format below should never reach the user, the one that matters is the + // one from the formatString parameter. + UnicodeString utf16SourceDateTimeFormat = UnicodeString::fromUTF8("No format specified (you should not be seeing this string!)"); + + SimpleDateFormat* dateFormatter = new SimpleDateFormat(utf16SourceDateTimeFormat, currentLocale, success); + dateFormatter->applyLocalizedPattern(utf16LocalizedDateTimeFormat, success); + dateFormatter->format(milliseconds, utf16Date); + delete dateFormatter; + + char utf8Date[512]; + CheckedArrayByteSink sink(utf8Date, ARRAY_SIZE(utf8Date)); + utf16Date.toUTF8(sink); + ENSURE(!sink.Overflowed()); + + return std::string(utf8Date, sink.NumberOfBytesWritten()); +} + +std::string L10n::FormatDecimalNumberIntoString(double number) +{ + UErrorCode success = U_ZERO_ERROR; + UnicodeString utf16Number; + NumberFormat* numberFormatter = NumberFormat::createInstance(currentLocale, UNUM_DECIMAL, success); + numberFormatter->format(number, utf16Number); + char utf8Number[512]; + CheckedArrayByteSink sink(utf8Number, ARRAY_SIZE(utf8Number)); + utf16Number.toUTF8(sink); + ENSURE(!sink.Overflowed()); + + return std::string(utf8Number, sink.NumberOfBytesWritten()); +} + +VfsPath L10n::LocalizePath(VfsPath sourcePath) +{ + VfsPath path = sourcePath; + + VfsPath localizedPath = sourcePath.Parent() / L"l10n" / wstring_from_utf8(currentLocale.getLanguage()) / sourcePath.Filename(); + if (VfsFileExists(localizedPath)) + path = localizedPath; + + return path; +} + +void L10n::LoadDictionaryForCurrentLocale() +{ + delete dictionary; + dictionary = new tinygettext::Dictionary(); + + VfsPaths filenames; + if (vfs::GetPathnames(g_VFS, L"l10n/", (wstring_from_utf8(currentLocale.getBaseName()) + L".*.po").c_str(), filenames) < 0) + return; + + // If not matching country is found, try to fall back to a matching language + if (filenames.size() == 0) + { + if (vfs::GetPathnames(g_VFS, L"l10n/", (wstring_from_utf8(currentLocale.getLanguage()) + L".*.po").c_str(), filenames) < 0) + return; + } + + for (VfsPaths::iterator it = filenames.begin(); it != filenames.end(); ++it) + { + VfsPath filename = *it; + CVFSFile file; + file.Load(g_VFS, filename); + std::string content = file.DecodeUTF8(); + ReadPoIntoDictionary(content, dictionary); + } +} + +void L10n::LoadListOfAvailableLocales() +{ + for (std::vector::iterator iterator = availableLocales.begin(); iterator != availableLocales.end(); ++iterator) + delete *iterator; + + availableLocales.clear(); + + Locale* defaultLocale = new Locale(Locale::getUS()); + availableLocales.push_back(defaultLocale); // Always available. + + VfsPaths filenames; + if (vfs::GetPathnames(g_VFS, L"l10n/", L"*.po", filenames) < 0) + return; + + for (VfsPaths::iterator it = filenames.begin(); it != filenames.end(); ++it) + { + // Note: PO files follow this naming convention: “l10n/..po”. For example: “l10n/gl.public.po”. + VfsPath filepath = *it; + std::string filename = utf8_from_wstring(filepath.string()).substr(strlen("l10n/")); + std::size_t lengthToFirstDot = filename.find('.'); + std::string localeCode = filename.substr(0, lengthToFirstDot); + Locale* locale = new Locale(Locale::createCanonical(localeCode.c_str())); + + bool localeIsAlreadyAvailable = false; + for (std::vector::iterator iterator = availableLocales.begin(); iterator != availableLocales.end(); ++iterator) + { + if (*locale == **iterator) + { + localeIsAlreadyAvailable = true; + break; + } + } + + if (!localeIsAlreadyAvailable) + availableLocales.push_back(locale); + else + delete locale; + } +} + +void L10n::ReadPoIntoDictionary(const std::string& poContent, tinygettext::Dictionary* dictionary) +{ + try + { + std::istringstream inputStream(poContent); + tinygettext::POParser::parse("virtual PO file", inputStream, *dictionary, false); + } + catch(std::exception& e) + { + LOGERROR(L"[Localization] Exception while reading virtual PO file: %hs", e.what()); + } +} + +DateFormat* L10n::CreateDateTimeInstance(L10n::DateTimeType type, DateFormat::EStyle style, const Locale& locale) +{ + switch(type) + { + case Date: + return SimpleDateFormat::createDateInstance(style, locale); + + case Time: + return SimpleDateFormat::createTimeInstance(style, locale); + + case DateTime: + default: + return SimpleDateFormat::createDateTimeInstance(style, style, locale); + } +} diff --git a/source/i18n/L10n.h b/source/i18n/L10n.h new file mode 100644 index 0000000000..8fc616dde2 --- /dev/null +++ b/source/i18n/L10n.h @@ -0,0 +1,95 @@ +/* Copyright (c) 2014 Wildfire Games + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef L10N_H +#define L10N_H + +#include +#include + +#include "lib/code_annotation.h" +#include "lib/external_libraries/icu.h" +#include "lib/external_libraries/tinygettext.h" + +#include "lib/file/vfs/vfs_path.h" + +class L10n +{ + NONCOPYABLE(L10n); +public: + + L10n(); + ~L10n(); + + enum DateTimeType { DateTime, Date, Time }; + + static L10n& Instance(); + + Locale GetCurrentLocale(); + std::vector GetAllLocales(); + bool SaveLocale(const std::string& localeCode); + bool SaveLocale(Locale locale); + std::vector GetSupportedLocaleBaseNames(); + std::vector GetSupportedLocaleDisplayNames(); + std::string GetCurrentLocaleString(); + std::string GetLocaleLanguage(const std::string& locale); + std::string GetLocaleBaseName(const std::string& locale); + std::string GetLocaleCountry(const std::string& locale); + std::string GetLocaleScript(const std::string& locale); + std::vector GetDictionariesForDictLocale(const std::string& locale); + std::string GetDictionaryLocale(std::string configLocaleString); + void GetDictionaryLocale(std::string configLocaleString, Locale& outLocale); + void ReevaluateCurrentLocaleAndReload(); + bool ValidateLocale(Locale locale); + bool ValidateLocale(const std::string& localeCode); + + std::string Translate(const std::string& sourceString); + std::string TranslateWithContext(const std::string& context, const std::string& sourceString); + std::string TranslatePlural(const std::string& singularSourceString, const std::string& pluralSourceString, int number); + std::string TranslatePluralWithContext(const std::string& context, const std::string& singularSourceString, const std::string& pluralSourceString, int number); + std::string TranslateLines(const std::string& sourceString); + + UDate ParseDateTime(const std::string& dateTimeString, const std::string& dateTimeFormat, const Locale& locale); + std::string LocalizeDateTime(const UDate& dateTime, DateTimeType type, DateFormat::EStyle style); + std::string FormatMillisecondsIntoDateString(UDate milliseconds, const std::string& formatString); + std::string FormatDecimalNumberIntoString(double number); + + VfsPath LocalizePath(VfsPath sourcePath); + +private: + + tinygettext::Dictionary* dictionary; + bool isRtlLanguage; + Locale currentLocale; + std::vector availableLocales; + bool currentLocaleIsOriginalGameLocale; + + Locale AutoDetectLocale(); + void LoadDictionaryForCurrentLocale(); + void LoadListOfAvailableLocales(); + + void ReadPoIntoDictionary(const std::string& poContent, tinygettext::Dictionary* dictionary); + + DateFormat* CreateDateTimeInstance(DateTimeType type, DateFormat::EStyle style, const Locale& locale); +}; + +#endif // L10N_H diff --git a/source/i18n/scripting/JSInterface_L10n.cpp b/source/i18n/scripting/JSInterface_L10n.cpp new file mode 100644 index 0000000000..7054af1c9a --- /dev/null +++ b/source/i18n/scripting/JSInterface_L10n.cpp @@ -0,0 +1,178 @@ +/* Copyright (C) 2014 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "JSInterface_L10n.h" + +#include "i18n/L10n.h" +#include "lib/utf8.h" +#include "ps/ConfigDB.h" + +// Returns a translation of the specified English string into the current language. +std::wstring JSI_L10n::Translate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring sourceString) +{ + return wstring_from_utf8(L10n::Instance().Translate(utf8_from_wstring(sourceString))); +} + +// Returns a translation of the specified English string, for the specified context. +std::wstring JSI_L10n::TranslateWithContext(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string context, std::wstring sourceString) +{ + return wstring_from_utf8(L10n::Instance().TranslateWithContext(context, utf8_from_wstring(sourceString))); +} + +// Return a translated version of the given strings (singular and plural) depending on an integer value. +std::wstring JSI_L10n::TranslatePlural(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring singularSourceString, std::wstring pluralSourceString, int number) +{ + return wstring_from_utf8(L10n::Instance().TranslatePlural(utf8_from_wstring(singularSourceString), utf8_from_wstring(pluralSourceString), number)); +} + +// Return a translated version of the given strings (singular and plural) depending on an integer value, for the specified context. +std::wstring JSI_L10n::TranslatePluralWithContext(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string context, std::wstring singularSourceString, std::wstring pluralSourceString, int number) +{ + return wstring_from_utf8(L10n::Instance().TranslatePluralWithContext(context, utf8_from_wstring(singularSourceString), utf8_from_wstring(pluralSourceString), number)); +} + +// Return a translated version of the given string, localizing it line by line. +std::wstring JSI_L10n::TranslateLines(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring sourceString) +{ + return wstring_from_utf8(L10n::Instance().TranslateLines(utf8_from_wstring(sourceString))); +} + +// Return a translated version of the items in the specified array. +std::vector JSI_L10n::TranslateArray(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::vector sourceArray) +{ + std::vector translatedArray; + for (std::vector::iterator iterator = sourceArray.begin(); iterator != sourceArray.end(); ++iterator) + { + translatedArray.push_back(wstring_from_utf8(L10n::Instance().Translate(utf8_from_wstring(*iterator)))); + } + return translatedArray; +} + +// Return a translated version of the given decimal number. +std::wstring JSI_L10n::MarkToTranslate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring sourceString) +{ + return sourceString; +} + +// Return a localized version of a time given in milliseconds. +std::wstring JSI_L10n::FormatMillisecondsIntoDateString(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), UDate milliseconds, std::wstring formatString) +{ + return wstring_from_utf8(L10n::Instance().FormatMillisecondsIntoDateString(milliseconds, utf8_from_wstring(formatString))); +} + +// Return a localized version of the given decimal number. +std::wstring JSI_L10n::FormatDecimalNumberIntoString(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), double number) +{ + return wstring_from_utf8(L10n::Instance().FormatDecimalNumberIntoString(number)); +} + +std::vector JSI_L10n::GetSupportedLocaleBaseNames(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) +{ + return L10n::Instance().GetSupportedLocaleBaseNames(); +} + +std::vector JSI_L10n::GetSupportedLocaleDisplayNames(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) +{ + return L10n::Instance().GetSupportedLocaleDisplayNames(); +} + +std::string JSI_L10n::GetCurrentLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) +{ + return L10n::Instance().GetCurrentLocaleString(); +} + +std::vector JSI_L10n::GetAllLocales(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) +{ + return L10n::Instance().GetAllLocales(); +} + +std::string JSI_L10n::GetDictionaryLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string configLocale) +{ + return L10n::Instance().GetDictionaryLocale(configLocale); +} + +std::vector JSI_L10n::GetDictionariesForDictLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().GetDictionariesForDictLocale(locale); +} + +std::string JSI_L10n::GetLocaleLanguage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().GetLocaleLanguage(locale); +} + +std::string JSI_L10n::GetLocaleBaseName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().GetLocaleBaseName(locale); +} + +std::string JSI_L10n::GetLocaleCountry(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().GetLocaleCountry(locale); +} + +std::string JSI_L10n::GetLocaleScript(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().GetLocaleScript(locale); +} + +bool JSI_L10n::ValidateLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().ValidateLocale(locale); +} + +bool JSI_L10n::SaveLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale) +{ + return L10n::Instance().SaveLocale(locale); +} + +void JSI_L10n::ReevaluateCurrentLocaleAndReload(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) +{ + L10n::Instance().ReevaluateCurrentLocaleAndReload(); +} + + +void JSI_L10n::RegisterScriptFunctions(ScriptInterface& scriptInterface) +{ + scriptInterface.RegisterFunction("Translate"); + scriptInterface.RegisterFunction("TranslateWithContext"); + scriptInterface.RegisterFunction("TranslatePlural"); + scriptInterface.RegisterFunction("TranslatePluralWithContext"); + scriptInterface.RegisterFunction("TranslateLines"); + scriptInterface.RegisterFunction, std::vector, &TranslateArray>("TranslateArray"); + scriptInterface.RegisterFunction("MarkToTranslate"); + scriptInterface.RegisterFunction("FormatMillisecondsIntoDateString"); + scriptInterface.RegisterFunction("FormatDecimalNumberIntoString"); + + scriptInterface.RegisterFunction, &GetSupportedLocaleBaseNames>("GetSupportedLocaleBaseNames"); + scriptInterface.RegisterFunction, &GetSupportedLocaleDisplayNames>("GetSupportedLocaleDisplayNames"); + scriptInterface.RegisterFunction("GetCurrentLocale"); + scriptInterface.RegisterFunction, &GetAllLocales>("GetAllLocales"); + scriptInterface.RegisterFunction("GetDictionaryLocale"); + scriptInterface.RegisterFunction, std::string, &GetDictionariesForDictLocale>("GetDictionariesForDictLocale"); + + scriptInterface.RegisterFunction("GetLocaleLanguage"); + scriptInterface.RegisterFunction("GetLocaleBaseName"); + scriptInterface.RegisterFunction("GetLocaleCountry"); + scriptInterface.RegisterFunction("GetLocaleScript"); + + scriptInterface.RegisterFunction("ValidateLocale"); + scriptInterface.RegisterFunction("SaveLocale"); + scriptInterface.RegisterFunction("ReevaluateCurrentLocaleAndReload"); +} diff --git a/source/i18n/scripting/JSInterface_L10n.h b/source/i18n/scripting/JSInterface_L10n.h new file mode 100644 index 0000000000..84a8626e1e --- /dev/null +++ b/source/i18n/scripting/JSInterface_L10n.h @@ -0,0 +1,55 @@ +/* Copyright (C) 2014 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#ifndef INCLUDED_JSINTERFACE_L10N +#define INCLUDED_JSINTERFACE_L10N + +#include "scriptinterface/ScriptInterface.h" +#include "lib/external_libraries/icu.h" + +namespace JSI_L10n +{ + void RegisterScriptFunctions(ScriptInterface& ScriptInterface); + + std::wstring Translate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring sourceString); + std::wstring TranslateWithContext(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string context, std::wstring sourceString); + std::wstring TranslatePlural(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring singularSourceString, std::wstring pluralSourceString, int number); + std::wstring TranslatePluralWithContext(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string context, std::wstring singularSourceString, std::wstring pluralSourceString, int number); + std::wstring TranslateLines(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring sourceString); + std::vector TranslateArray(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::vector sourceArray); + std::wstring MarkToTranslate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring sourceString); + std::wstring FormatMillisecondsIntoDateString(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), UDate milliseconds, std::wstring formatString); + std::wstring FormatDecimalNumberIntoString(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), double number); + + std::vector GetSupportedLocaleBaseNames(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)); + std::vector GetSupportedLocaleDisplayNames(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)); + std::string GetCurrentLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)); + std::vector GetAllLocales(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)); + std::string GetDictionaryLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string configLocale); + std::vector GetDictionariesForDictLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + + std::string GetLocaleLanguage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + std::string GetLocaleBaseName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + std::string GetLocaleCountry(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + std::string GetLocaleScript(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + + bool ValidateLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + bool SaveLocale(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string locale); + void ReevaluateCurrentLocaleAndReload(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)); +} + +#endif diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 59c50a04c9..37c8b76e85 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -116,6 +116,9 @@ shared_ptr g_ScriptRuntime; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code +bool g_InDevelopmentCopy; +bool g_CheckedIfInDevelopmentCopy = false; + static void SetTextureQuality(int quality) { int q_flags; @@ -389,7 +392,7 @@ ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED( return ERI_NOT_IMPLEMENTED; } -static std::vector GetMods(const CmdLineArgs& args, bool dev) +static std::vector GetMods(const CmdLineArgs& args) { std::vector mods = args.GetMultiple("mod"); // List of the mods, to be used by the Gui @@ -401,7 +404,7 @@ static std::vector GetMods(const CmdLineArgs& args, bool dev) // Add the user mod if not explicitly disabled or we have a dev copy so // that saved files end up in version control and not in the user mod. - if (!dev && !args.Has("noUserMod")) + if (!InDevelopmentCopy() && !args.Has("noUserMod")) mods.push_back("user"); return mods; @@ -435,9 +438,11 @@ static void InitVfs(const CmdLineArgs& args, int flags) // (maps, etc) end up in version control. const OsPath readonlyConfig = paths.RData()/"config"/""; g_VFS->Mount(L"config/", readonlyConfig); - bool dev = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); - const std::vector mods = GetMods(args, dev); + // Engine localization files. + g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); + + const std::vector mods = GetMods(args); OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; @@ -448,7 +453,7 @@ static void InitVfs(const CmdLineArgs& args, int flags) size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; OsPath modName(mods[i]); - if (dev) + if (InDevelopmentCopy()) { // We are running a dev copy, so only mount mods in the user mod path // if the mod does not exist in the data path. @@ -1321,3 +1326,13 @@ void CancelLoad(const CStrW& message) g_GUI->GetActiveGUI()->GetScriptInterface()->CallFunctionVoid(g_GUI->GetActiveGUI()->GetGlobalObject(), "cancelOnError", message); } } + +bool InDevelopmentCopy() +{ + if (!g_CheckedIfInDevelopmentCopy) + { + g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); + g_CheckedIfInDevelopmentCopy = true; + } + return g_InDevelopmentCopy; +} diff --git a/source/ps/GameSetup/GameSetup.h b/source/ps/GameSetup/GameSetup.h index 87ae0226c2..d9b588ac02 100644 --- a/source/ps/GameSetup/GameSetup.h +++ b/source/ps/GameSetup/GameSetup.h @@ -67,4 +67,6 @@ extern void InitGraphics(const CmdLineArgs& args, int flags); extern void Shutdown(int flags); extern void CancelLoad(const CStrW& message); +extern bool InDevelopmentCopy(); + #endif // INCLUDED_GAMESETUP diff --git a/source/ps/SavedGame.cpp b/source/ps/SavedGame.cpp index 3347ff4159..fc0efee3ea 100644 --- a/source/ps/SavedGame.cpp +++ b/source/ps/SavedGame.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2014 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -23,6 +23,8 @@ #include "gui/GUIManager.h" #include "lib/allocators/shared_ptr.h" #include "lib/file/archive/archive_zip.h" +#include "i18n/L10n.h" +#include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" @@ -121,7 +123,7 @@ Status SavedGames::Save(const std::wstring& name, const std::wstring& descriptio OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); - LOGMESSAGERENDER(L"Saved game to '%ls'\n", realPath.string().c_str()); + LOGMESSAGERENDER(wstring_from_utf8(L10n::Instance().Translate("Saved game to '%ls'") + "\n").c_str(), realPath.string().c_str()); return INFO::OK; }