pioneer/src/Lang.cpp

232 lines
6.4 KiB
C++

// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
#include "Lang.h"
#include "FileSystem.h"
#include "JsonUtils.h"
#include "StringRange.h"
#include "libs.h"
#include "text/TextSupport.h"
#include "utils.h"
#include <map>
#include <set>
namespace Lang {
static bool ident_head(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c == '_');
}
static bool ident_tail(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_');
}
static bool valid_token(const std::string &token)
{
if (token.empty()) return false;
if (!ident_head(token[0])) return false;
for (unsigned int i = 1; i < token.size(); i++)
if (!ident_tail(token[i])) return false;
return true;
}
bool Resource::Load()
{
if (m_loaded)
return true;
std::string filename = "lang/" + m_name + "/" + m_langCode + ".json";
Json data = JsonUtils::LoadJsonDataFile(filename);
if (data.is_null()) {
Output("couldn't read language file '%s'\n", filename.c_str());
return false;
}
for (Json::iterator i = data.begin(); i != data.end(); ++i) {
const std::string token = i.key();
if (token.empty()) {
Output("%s: found empty token, skipping it\n", filename.c_str());
continue;
}
if (!valid_token(token)) {
Output("%s: invalid token '%s', skipping it\n", filename.c_str(), token.c_str());
continue;
}
Json message = i.value()["message"];
if (message.is_null()) {
Output("%s: no 'message' key for token '%s', skipping it\n", filename.c_str(), token.c_str());
continue;
}
if (!message.is_string()) {
Output("%s: value for token '%s' is not a string, skipping it\n", filename.c_str(), token.c_str());
continue;
}
std::string text = message;
if (text.empty()) {
Output("%s: empty value for token '%s', skipping it\n", filename.c_str(), token.c_str());
continue;
}
// extracted quoted string
if (text[0] == '"' && text[text.size() - 1] == '"')
text = text.substr(1, text.size() - 2);
// adjust for escaped newlines
{
std::string adjustedText;
adjustedText.reserve(text.size());
unsigned int ii;
for (ii = 0; ii < text.size() - 1; ii++) {
const char *c = &text[ii];
if (c[0] == '\\' && c[1] == 'n') {
ii++;
adjustedText += '\n';
} else
adjustedText += *c;
}
if (ii != text.size())
adjustedText += text[ii++];
assert(ii == text.size());
text = adjustedText;
}
m_strings[token] = text;
}
m_loaded = true;
return true;
}
const std::string &Resource::Get(const std::string &token) const
{
std::map<std::string, std::string>::const_iterator i = m_strings.find(token);
if (i == m_strings.end()) {
static const std::string empty;
return empty;
}
return (*i).second;
}
std::vector<std::string> Resource::GetAvailableLanguages(const std::string &resourceName)
{
std::vector<std::string> languages;
for (FileSystem::FileEnumerator files(FileSystem::gameDataFiles, "lang/" + resourceName); !files.Finished(); files.Next()) {
assert(files.Current().IsFile());
const std::string &path = files.Current().GetPath();
if (ends_with_ci(path, ".json")) {
const std::string name = files.Current().GetName();
languages.push_back(name.substr(0, name.size() - 5));
}
}
return languages;
}
// XXX we're allocating a KB for each translatable string
// that's... not very nice (though I guess it "doesn't matter" with virtual memory and multi-GB of RAM)
static const int STRING_RECORD_SIZE = 1024;
#define DECLARE_STRING(x) char x[STRING_RECORD_SIZE];
#include "LangStrings.inc.h"
#undef DECLARE_STRING
//
// declaring value type as const char* so that we can give out a const reference to the real
// token map without allowing external code to modify token text
// unfortunately, this means we don't have write access internally either,
// so we have to const_cast<> to initialise the token values.
// this could be avoided by using a custom class for the value type
// (or std::string, but then we'd be changing the way translated text is stored)
typedef std::map<std::string, const char *> token_map;
static token_map s_token_map;
static struct init_string_helper_class {
init_string_helper_class()
{
#define DECLARE_STRING(x) \
s_token_map.insert(std::make_pair(#x, Lang::x)); \
Lang::x[0] = '\0';
#include "LangStrings.inc.h"
#undef DECLARE_STRING
}
} init_string_helper;
static void copy_string(char *buf, const char *str, size_t strsize, size_t bufsize)
{
size_t sz = std::min(strsize, bufsize - 1);
memcpy(buf, str, sz);
buf[sz] = '\0';
}
static Resource s_coreResource("core", "<unknown>");
void MakeCore(Resource &res)
{
assert(res.GetName() == "core");
res.Load();
for (token_map::iterator i = s_token_map.begin(); i != s_token_map.end(); ++i) {
const std::string &token = i->first;
std::string text = res.Get(token);
if (text.empty()) {
Output("%s/%s: token '%s' not found\n", res.GetName().c_str(), res.GetLangCode().c_str(), token.c_str());
text = token;
}
if (text.size() > size_t(STRING_RECORD_SIZE)) {
Output("%s/%s: text for token '%s' is too long and will be truncated\n", res.GetName().c_str(), res.GetLangCode().c_str(), token.c_str());
text.resize(STRING_RECORD_SIZE);
}
// const_cast so we can set the string, see above
char *record = const_cast<char *>(i->second);
copy_string(record, text.c_str(), text.size(), STRING_RECORD_SIZE);
}
s_coreResource = res;
}
const Resource &GetCore()
{
return s_coreResource;
}
static std::map<std::string, Resource> m_cachedResources;
Resource GetResource(const std::string &name, const std::string &langCode)
{
auto key = name + ":" + langCode;
auto i = m_cachedResources.find(key);
if (i != m_cachedResources.end())
return i->second;
Lang::Resource res = Lang::Resource(name, langCode);
bool loaded = res.Load();
if (!loaded) {
if (langCode != "en") {
Output("couldn't load language resource %s/%s, trying %s/en\n", name.c_str(), langCode.c_str(), name.c_str());
res = Lang::Resource(name, "en");
loaded = res.Load();
key = name + ":" + "en";
}
if (!loaded)
Output("couldn't load language resource %s/en\n", name.c_str());
}
if (loaded)
m_cachedResources.insert(std::make_pair(key, res));
return res;
}
} // namespace Lang