mirror of https://github.com/aseprite/aseprite.git
256 lines
6.3 KiB
C++
256 lines
6.3 KiB
C++
// Aseprite
|
|
// Copyright (C) 2023-2024 Igara Studio S.A.
|
|
// Copyright (C) 2016-2018 David Capello
|
|
//
|
|
// This program is distributed under the terms of
|
|
// the End-User License Agreement for Aseprite.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/i18n/strings.h"
|
|
|
|
#include "app/app.h"
|
|
#include "app/extensions.h"
|
|
#include "app/pref/preferences.h"
|
|
#include "app/resource_finder.h"
|
|
#include "app/xml_document.h"
|
|
#include "app/xml_exception.h"
|
|
#include "base/debug.h"
|
|
#include "base/fs.h"
|
|
#include "cfg/cfg.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace app {
|
|
|
|
static Strings* singleton = nullptr;
|
|
|
|
const char* Strings::kDefLanguage = "en";
|
|
|
|
// static
|
|
void Strings::createInstance(Preferences& pref,
|
|
Extensions& exts)
|
|
{
|
|
ASSERT(!singleton);
|
|
singleton = new Strings(pref, exts);
|
|
}
|
|
|
|
// static
|
|
Strings* Strings::instance()
|
|
{
|
|
return singleton;
|
|
}
|
|
|
|
Strings::Strings(Preferences& pref,
|
|
Extensions& exts)
|
|
: m_pref(pref)
|
|
, m_exts(exts)
|
|
{
|
|
loadLanguage(currentLanguage());
|
|
}
|
|
|
|
std::set<LangInfo> Strings::availableLanguages() const
|
|
{
|
|
std::set<LangInfo> result;
|
|
|
|
// Add languages in data/strings/ + data/strings.git/
|
|
ResourceFinder rf;
|
|
rf.includeDataDir("strings");
|
|
rf.includeDataDir("strings.git");
|
|
while (rf.next()) {
|
|
const std::string stringsPath = rf.filename();
|
|
if (!base::is_directory(stringsPath))
|
|
continue;
|
|
|
|
for (const auto& fn : base::list_files(stringsPath)) {
|
|
// Ignore README/LICENSE files.
|
|
if (base::get_file_extension(fn) != "ini")
|
|
continue;
|
|
|
|
const std::string langId = base::get_file_title(fn);
|
|
std::string path = base::join_path(stringsPath, fn);
|
|
std::string displayName = langId;
|
|
|
|
// Load display name
|
|
cfg::CfgFile cfg;
|
|
if (cfg.load(path))
|
|
displayName = cfg.getValue("_", "display_name", displayName.c_str());
|
|
|
|
result.insert(LangInfo(langId, path, displayName));
|
|
}
|
|
}
|
|
|
|
// Add languages in extensions
|
|
for (const auto& ext : m_exts) {
|
|
if (ext->isEnabled() &&
|
|
ext->hasLanguages()) {
|
|
for (const auto& lang : ext->languages())
|
|
result.insert(lang.second);
|
|
}
|
|
}
|
|
|
|
// Check that the default language exists.
|
|
ASSERT(std::find_if(result.begin(), result.end(),
|
|
[](const LangInfo& li){
|
|
return li.id == kDefLanguage;
|
|
}) != result.end());
|
|
|
|
return result;
|
|
}
|
|
|
|
std::string Strings::currentLanguage() const
|
|
{
|
|
return m_pref.general.language();
|
|
}
|
|
|
|
void Strings::setCurrentLanguage(const std::string& langId)
|
|
{
|
|
// Do nothing (same language)
|
|
if (currentLanguage() == langId)
|
|
return;
|
|
|
|
m_pref.general.language(langId);
|
|
loadLanguage(langId);
|
|
|
|
LanguageChange();
|
|
}
|
|
|
|
void Strings::logError(const char* id, const char* error) const
|
|
{
|
|
LOG(ERROR, "I18N: Error in \"%s.ini\" file, string id \"%s\" is \"%s\", threw \"%s\" error\n",
|
|
currentLanguage().c_str(), id, translate(id).c_str(), error);
|
|
}
|
|
|
|
// static
|
|
std::string Strings::VFormat(const char* id, const fmt::format_args& vargs)
|
|
{
|
|
Strings* s = Strings::instance();
|
|
|
|
// First we try to translate the ID string with the current
|
|
// translation. If it fails (e.g. a fmt::format_error) it can be
|
|
// because an ill-formed string for the fmt library.
|
|
//
|
|
// In that case then we try to use the regular English string from
|
|
// the en.ini (which can fail too if the user modified the en.ini
|
|
// file by hand).
|
|
try {
|
|
return fmt::vformat(s->translate(id), vargs);
|
|
}
|
|
catch (const std::runtime_error& e) {
|
|
s->logError(id, e.what());
|
|
// Continue with default translation (English)...
|
|
}
|
|
|
|
try {
|
|
return fmt::vformat(s->defaultString(id), vargs);
|
|
}
|
|
catch (const std::runtime_error& e) {
|
|
// This is unexpected, invalid en.ini file
|
|
ASSERT(false);
|
|
s->logError(id, e.what());
|
|
return id;
|
|
}
|
|
}
|
|
|
|
void Strings::loadLanguage(const std::string& langId)
|
|
{
|
|
m_strings.clear();
|
|
loadStringsFromDataDir(kDefLanguage);
|
|
m_default = m_strings;
|
|
|
|
if (langId != kDefLanguage) {
|
|
loadStringsFromDataDir(langId);
|
|
loadStringsFromExtension(langId);
|
|
}
|
|
}
|
|
|
|
void Strings::loadStringsFromDataDir(const std::string& langId)
|
|
{
|
|
// Load the English language file from the Aseprite data directory (so we have the most update list of strings)
|
|
LOG("I18N: Loading %s.ini file\n", langId.c_str());
|
|
ResourceFinder rf;
|
|
rf.includeDataDir(base::join_path("strings", langId + ".ini").c_str());
|
|
rf.includeDataDir(base::join_path("strings.git", langId + ".ini").c_str());
|
|
if (!rf.findFirst()) {
|
|
LOG("I18N: %s.ini was not found\n", langId.c_str());
|
|
return;
|
|
}
|
|
LOG("I18N: %s found\n", rf.filename().c_str());
|
|
loadStringsFromFile(rf.filename());
|
|
}
|
|
|
|
void Strings::loadStringsFromExtension(const std::string& langId)
|
|
{
|
|
std::string fn = m_exts.languagePath(langId);
|
|
if (!fn.empty() && base::is_file(fn))
|
|
loadStringsFromFile(fn);
|
|
}
|
|
|
|
void Strings::loadStringsFromFile(const std::string& fn)
|
|
{
|
|
cfg::CfgFile cfg;
|
|
cfg.load(fn);
|
|
|
|
std::vector<std::string> sections;
|
|
std::vector<std::string> keys;
|
|
cfg.getAllSections(sections);
|
|
for (const auto& section : sections) {
|
|
keys.clear();
|
|
cfg.getAllKeys(section.c_str(), keys);
|
|
|
|
std::string textId = section;
|
|
std::string value;
|
|
textId.push_back('.');
|
|
for (auto key : keys) {
|
|
textId.append(key);
|
|
|
|
value = cfg.getValue(section.c_str(), key.c_str(), "");
|
|
|
|
// Process escaped chars (\\, \n, \s, \t, etc.)
|
|
for (int i=0; i<int(value.size()); ) {
|
|
if (value[i] == '\\') {
|
|
value.erase(i, 1);
|
|
if (i == int(value.size()))
|
|
break;
|
|
int chr = value[i];
|
|
switch (chr) {
|
|
case '\\': chr = '\\'; break;
|
|
case 'n': chr = '\n'; break;
|
|
case 't': chr = '\t'; break;
|
|
case 's': chr = ' '; break;
|
|
}
|
|
value[i] = chr;
|
|
}
|
|
else {
|
|
++i;
|
|
}
|
|
}
|
|
m_strings[textId] = value;
|
|
|
|
//TRACE("I18N: Reading string %s -> %s\n", textId.c_str(), m_strings[textId].c_str());
|
|
|
|
textId.erase(section.size()+1);
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::string& Strings::translate(const char* id) const
|
|
{
|
|
auto it = m_strings.find(id);
|
|
if (it != m_strings.end())
|
|
return it->second;
|
|
return m_strings[id] = id;
|
|
}
|
|
|
|
const std::string& Strings::defaultString(const char* id) const
|
|
{
|
|
auto it = m_default.find(id);
|
|
if (it != m_default.end())
|
|
return it->second;
|
|
return m_default[id] = id;
|
|
}
|
|
|
|
} // namespace app
|