/* * Copyright 2010-2016 OpenXcom Developers. * * This file is part of OpenXcom. * * OpenXcom 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. * * OpenXcom 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 OpenXcom. If not, see . */ #include "Language.h" #include #include #include #include #include #include "CrossPlatform.h" #include "Logger.h" #include "Options.h" #include "LanguagePlurality.h" #include "Unicode.h" #include "Exception.h" #include "../Mod/ExtraStrings.h" namespace OpenXcom { std::map Language::_names; std::vector Language::_rtl, Language::_cjk; /** * Initializes an empty language file. */ Language::Language() : _handler(0), _direction(DIRECTION_LTR), _wrap(WRAP_WORDS) { // maps don't have initializers :( if (_names.empty()) { _names["en-US"] = "English (US)"; _names["en-GB"] = "English (UK)"; // _names["ar"] = "العربية"; needs fonts _names["bg"] = "Български"; _names["ca-ES"] = "Català"; _names["cs"] = "Česky"; _names["cy"] = "Cymraeg"; _names["da"] = "Dansk"; _names["de"] = "Deutsch"; _names["el"] = "Ελληνικά"; _names["et"] = "Eesti"; _names["es-ES"] = "Español (ES)"; _names["es-419"] = "Español (AL)"; _names["fr"] = "Français (FR)"; _names["fr-CA"] = "Français (CA)"; _names["fi"] = "Suomi"; _names["ga"] = "Gaeilge"; _names["hr"] = "Hrvatski"; _names["hu"] = "Magyar"; _names["it"] = "Italiano"; _names["is"] = "Íslenska"; _names["ja"] = "日本語"; _names["ko"] = "한국어"; _names["lb"] = "Lëtzebuergesch"; _names["lv"] = "Latviešu"; _names["nl"] = "Nederlands"; _names["no"] = "Norsk"; _names["pl"] = "Polski"; _names["pt-BR"] = "Português (BR)"; _names["pt-PT"] = "Português (PT)"; _names["ro"] = "Română"; _names["ru"] = "Русский"; _names["sk"] = "Slovenčina"; _names["sl"] = "Slovenščina"; _names["sv"] = "Svenska"; // _names["th"] = "ไทย"; needs fonts _names["tr"] = "Türkçe"; _names["uk"] = "Українська"; _names["vi"] = "Tiếng Việt"; _names["zh-CN"] = "中文"; _names["zh-TW"] = "文言"; } if (_rtl.empty()) { //_rtl.push_back("he"); needs translation } if (_cjk.empty()) { _cjk.push_back("ja"); _cjk.push_back("ko"); _cjk.push_back("zh-CN"); _cjk.push_back("zh-TW"); } std::string id = Options::language; _handler = LanguagePlurality::create(id); if (std::find(_rtl.begin(), _rtl.end(), id) == _rtl.end()) { _direction = DIRECTION_LTR; } else { _direction = DIRECTION_RTL; } if (Options::wordwrap == WRAP_AUTO) { if (std::find(_cjk.begin(), _cjk.end(), id) == _cjk.end()) { _wrap = WRAP_WORDS; } else { _wrap = WRAP_LETTERS; } } else { _wrap = Options::wordwrap; } } /** * */ Language::~Language() { delete _handler; } /** * Gets all the languages found in the * Data folder and returns their properties. * @param files List of language filenames. * @param names List of language human-readable names. */ void Language::getList(std::vector &files, std::vector &names) { files = CrossPlatform::getFolderContents(CrossPlatform::searchDataFolder("common/Language"), "yml"); names.clear(); for (std::vector::iterator i = files.begin(); i != files.end(); ++i) { *i = CrossPlatform::noExt(*i); std::string name; std::map::iterator lang = _names.find(*i); if (lang != _names.end()) { name = lang->second; } else { name = *i; } names.push_back(name); } } /** * Loads a language file in Ruby-on-Rails YAML format. * Not that this has anything to do with Ruby, but since it's a * widely-supported format and we already have YAML, it was convenient. * @param filename Filename of the YAML file. */ void Language::load(const std::string &filename) { YAML::Node doc = YAML::LoadFile(filename); YAML::Node lang; if (doc.begin()->second.IsMap()) { lang = doc.begin()->second; } // Fallback when file is missing language specifier else { lang = doc; } for (YAML::const_iterator i = lang.begin(); i != lang.end(); ++i) { // Regular strings if (i->second.IsScalar()) { std::string value = i->second.as(); if (!value.empty()) { std::string key = i->first.as(); _strings[key] = loadString(value); } } // Strings with plurality else if (i->second.IsMap()) { for (YAML::const_iterator j = i->second.begin(); j != i->second.end(); ++j) { std::string value = j->second.as(); if (!value.empty()) { std::string key = i->first.as() + "_" + j->first.as(); _strings[key] = loadString(value); } } } } } /** * Loads a language file from an external path. * @param path Language file path. */ void Language::loadFile(const std::string &path) { try { if (CrossPlatform::fileExists(path)) { load(path); } } catch (YAML::Exception &e) { throw Exception(path + ": " + std::string(e.what())); } } /** * Loads a language file from a mod's ExtraStrings. * @param extraStrings List of ExtraStrings. * @param id Language ID. */ void Language::loadRule(const std::map &extraStrings, const std::string &id) { std::map::const_iterator it = extraStrings.find(id); if (it != extraStrings.end()) { ExtraStrings *extras = it->second; for (std::map::const_iterator i = extras->getStrings()->begin(); i != extras->getStrings()->end(); ++i) { _strings[i->first] = loadString(i->second); } } } /** * Replaces all special string markers with the appropriate characters. * @param string Original string. * @return New converted string. */ std::string Language::loadString(const std::string &string) const { std::string s = string; Unicode::replace(s, "{NEWLINE}", "\n"); Unicode::replace(s, "{SMALLLINE}", "\x02"); // Unicode::TOK_NL_SMALL Unicode::replace(s, "{ALT}", "\x01"); // Unicode::TOK_COLOR_FLIP return s; } /** * Returns the localized text with the specified ID. * If it's not found, just returns the ID. * @param id ID of the string. * @return String with the requested ID. */ LocalizedText Language::getString(const std::string &id) const { if (id.empty()) { return id; } std::map::const_iterator s = _strings.find(id); // Check if translation strings recently learned pluralization. if (s == _strings.end()) { return getString(id, UINT_MAX); } else { return s->second; } } /** * Returns the localized text with the specified ID, in the proper form for @a n. * The substitution of @a n has already happened in the returned LocalizedText. * If it's not found, just returns the ID. * @param id ID of the string. * @param n Number to use to decide the proper form. * @return String with the requested ID. */ LocalizedText Language::getString(const std::string &id, unsigned n) const { assert(!id.empty()); static std::set notFoundIds; std::map::const_iterator s = _strings.end(); // Try specialized form. if (n == 0) { s = _strings.find(id + "_zero"); } // Try proper form by language if (s == _strings.end()) { s = _strings.find(id + _handler->getSuffix(n)); } // Try default form if (s == _strings.end()) { s = _strings.find(id + "_other"); } // Give up if (s == _strings.end()) { if (notFoundIds.end() == notFoundIds.find(id)) { notFoundIds.insert(id); Log(LOG_WARNING) << id << " not found in " << Options::language; } return id; } if (n == UINT_MAX) // Special case { if (notFoundIds.end() == notFoundIds.find(id)) { notFoundIds.insert(id); Log(LOG_WARNING) << id << " has plural format in ``" << Options::language << "``. Code assumes singular format."; // Hint: Change ``getstring(ID).arg(value)`` to ``getString(ID, value)`` in appropriate files. } return s->second; } else { std::ostringstream ss; ss << n; std::string marker("{N}"), val(ss.str()), txt(s->second); Unicode::replace(txt, marker, val); return txt; } } /** * Returns the localized text with the specified ID, in the proper form for the gender. * If it's not found, just returns the ID. * @param id ID of the string. * @param gender Current soldier gender. * @return String with the requested ID. */ LocalizedText Language::getString(const std::string &id, SoldierGender gender) const { std::string genderId; if (gender == GENDER_MALE) { genderId = id + "_MALE"; } else { genderId = id + "_FEMALE"; } return getString(genderId); } /** * Outputs all the language IDs and strings * to an HTML table. * @param filename HTML file. */ void Language::toHtml(const std::string &filename) const { std::ofstream htmlFile (filename.c_str(), std::ios::out); htmlFile << "" << std::endl; htmlFile << "" << std::endl; for (std::map::const_iterator i = _strings.begin(); i != _strings.end(); ++i) { htmlFile << "" << std::endl; } htmlFile << "
ID StringEnglish String
" << i->first << ""; std::string s = i->second; for (std::string::const_iterator j = s.begin(); j != s.end(); ++j) { if (*j == Unicode::TOK_NL_SMALL || *j == '\n') { htmlFile << "
"; } else { htmlFile << *j; } } htmlFile << "
" << std::endl; htmlFile.close(); } /** * Returns the direction to use for rendering * text in this language. * @return Text direction. */ TextDirection Language::getTextDirection() const { return _direction; } /** * Returns the wrapping rules to use for rendering * text in this language. * @return Text wrapping. */ TextWrapping Language::getTextWrapping() const { return _wrap; } } /** @page LanguageFiles Format of the language files. Language files are formatted as YAML (.yml) containing UTF-8 (no BOM) text. The first line in a language file is the language's identifier. The rest of the file are key-value pairs. The key of each pair contains the ID string (dictionary key), and the value contains the localized text for the given key in quotes. The localized text may contain the following special markers:
{0, 1, 2, ... } These markers will be replaced by programmer-supplied values before the message is displayed.
{ALT} The rest of the text will be in an alternate color. Using this again will switch back to the primary color.
{NEWLINE} It will be replaced with a line break in the game.
{SMALLLINE} The rest of the text will be in a small font.
There is an additional marker sequence, that should only appear in texts that depend on a number. This marker {N} will be replaced by the actual number used. The keys for texts that depend on numbers also have special suffixes, that depend on the language. For all languages, a suffix of _zero is tried if the number is zero, before trying the actual key according to the language rules. The rest of the suffixes depend on the language, as described here. So, you would write (for English):
STR_ENEMIES:
  zero:  "There are no enemies left."
  one:   "There is a single enemy left."
  other: "There are {N} enemies left."
*/