mirror of
https://github.com/libretro/devilutionX.git
synced 2026-11-10 02:41:39 -10:00
When using `UNPACKED_MPQS`, avoid all the SDL machinery for reading files. This is beneficial not only due to reduced indirection but also because we can test for the file's existence and get the file size without opening it, which is much faster.
436 lines
11 KiB
C++
436 lines
11 KiB
C++
#include "utils/language.h"
|
|
|
|
#include <memory>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include <function_ref.hpp>
|
|
|
|
#include "engine/assets.hpp"
|
|
#include "options.h"
|
|
#include "utils/file_util.h"
|
|
#include "utils/log.hpp"
|
|
#include "utils/paths.h"
|
|
#include "utils/stdcompat/string_view.hpp"
|
|
|
|
#define MO_MAGIC 0x950412de
|
|
|
|
namespace {
|
|
|
|
using namespace devilution;
|
|
|
|
std::unique_ptr<char[]> translationKeys;
|
|
std::unique_ptr<char[]> translationValues;
|
|
|
|
using TranslationRef = uint32_t;
|
|
|
|
struct StringHash {
|
|
size_t operator()(const char *str) const noexcept
|
|
{
|
|
return std::hash<string_view> {}(str);
|
|
}
|
|
};
|
|
|
|
struct StringEq {
|
|
bool operator()(const char *lhs, const char *rhs) const noexcept
|
|
{
|
|
return string_view(lhs) == string_view(rhs);
|
|
}
|
|
};
|
|
|
|
std::vector<std::unordered_map<const char *, TranslationRef, StringHash, StringEq>> translation = { {}, {} };
|
|
|
|
constexpr uint32_t TranslationRefOffsetBits = 19;
|
|
constexpr uint32_t TranslationRefSizeBits = 32 - TranslationRefOffsetBits; // 13
|
|
constexpr uint32_t TranslationRefSizeMask = (1 << TranslationRefSizeBits) - 1;
|
|
|
|
TranslationRef EncodeTranslationRef(uint32_t offset, uint32_t size)
|
|
{
|
|
return (offset << TranslationRefSizeBits) | size;
|
|
}
|
|
|
|
string_view GetTranslation(TranslationRef ref)
|
|
{
|
|
return { &translationValues[ref >> TranslationRefSizeBits], ref & TranslationRefSizeMask };
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace {
|
|
|
|
struct MoHead {
|
|
uint32_t magic;
|
|
struct {
|
|
uint16_t major;
|
|
uint16_t minor;
|
|
} revision;
|
|
|
|
uint32_t nbMappings;
|
|
uint32_t srcOffset;
|
|
uint32_t dstOffset;
|
|
};
|
|
|
|
void SwapLE(MoHead &head)
|
|
{
|
|
head.magic = SDL_SwapLE32(head.magic);
|
|
head.revision.major = SDL_SwapLE16(head.revision.major);
|
|
head.revision.minor = SDL_SwapLE16(head.revision.minor);
|
|
head.nbMappings = SDL_SwapLE32(head.nbMappings);
|
|
head.srcOffset = SDL_SwapLE32(head.srcOffset);
|
|
head.dstOffset = SDL_SwapLE32(head.dstOffset);
|
|
}
|
|
|
|
struct MoEntry {
|
|
uint32_t length;
|
|
uint32_t offset;
|
|
};
|
|
|
|
void SwapLE(MoEntry &entry)
|
|
{
|
|
entry.length = SDL_SwapLE32(entry.length);
|
|
entry.offset = SDL_SwapLE32(entry.offset);
|
|
}
|
|
|
|
string_view TrimLeft(string_view str)
|
|
{
|
|
str.remove_prefix(std::min(str.find_first_not_of(" \t"), str.size()));
|
|
return str;
|
|
}
|
|
|
|
string_view TrimRight(string_view str)
|
|
{
|
|
str.remove_suffix(str.size() - (str.find_last_not_of(" \t") + 1));
|
|
return str;
|
|
}
|
|
|
|
// English, Danish, Spanish, Italian, Swedish
|
|
unsigned PluralForms = 2;
|
|
tl::function_ref<int(int n)> GetLocalPluralId = [](int n) -> int { return n != 1 ? 1 : 0; };
|
|
|
|
/**
|
|
* Match plural=(n != 1);"
|
|
*/
|
|
void SetPluralForm(string_view expression)
|
|
{
|
|
const string_view key = "plural=";
|
|
const string_view::size_type keyPos = expression.find(key);
|
|
if (keyPos == string_view::npos)
|
|
return;
|
|
expression.remove_prefix(keyPos + key.size());
|
|
|
|
const string_view::size_type semicolonPos = expression.find(';');
|
|
if (semicolonPos != string_view::npos) {
|
|
expression.remove_suffix(expression.size() - semicolonPos);
|
|
}
|
|
|
|
expression = TrimLeft(TrimRight(expression));
|
|
|
|
// ko, zh_CN, zh_TW
|
|
if (expression == "0") {
|
|
GetLocalPluralId = [](int /*n*/) -> int { return 0; };
|
|
return;
|
|
}
|
|
|
|
// en, bg, da, de, es, it, sv
|
|
if (expression == "(n != 1)") {
|
|
GetLocalPluralId = [](int n) -> int { return n != 1 ? 1 : 0; };
|
|
return;
|
|
}
|
|
|
|
// fr, pt_BR
|
|
if (expression == "(n > 1)") {
|
|
GetLocalPluralId = [](int n) -> int { return n > 1 ? 1 : 0; };
|
|
return;
|
|
}
|
|
|
|
// hr, ru
|
|
if (expression == "(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)") {
|
|
GetLocalPluralId = [](int n) -> int {
|
|
if (n % 10 == 1 && n % 100 != 11)
|
|
return 0;
|
|
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14))
|
|
return 1;
|
|
return 2;
|
|
};
|
|
return;
|
|
}
|
|
|
|
// pl
|
|
if (expression == "(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)") {
|
|
GetLocalPluralId = [](int n) -> int {
|
|
if (n == 1)
|
|
return 0;
|
|
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14))
|
|
return 1;
|
|
return 2;
|
|
};
|
|
return;
|
|
}
|
|
|
|
// ro
|
|
if (expression == "(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2)") {
|
|
GetLocalPluralId = [](int n) -> int {
|
|
if (n == 1)
|
|
return 0;
|
|
if (n == 0 || (n != 1 && n % 100 >= 1 && n % 100 <= 19))
|
|
return 1;
|
|
return 2;
|
|
};
|
|
return;
|
|
}
|
|
|
|
// cs
|
|
if (expression == "(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2") {
|
|
GetLocalPluralId = [](int n) -> int {
|
|
if (n == 1)
|
|
return 0;
|
|
if (n >= 2 && n <= 4)
|
|
return 1;
|
|
return 2;
|
|
};
|
|
return;
|
|
}
|
|
|
|
LogError("Unknown plural expression: '{}'", expression);
|
|
}
|
|
|
|
/**
|
|
* Parse "nplurals=2;"
|
|
*/
|
|
void ParsePluralForms(string_view string)
|
|
{
|
|
const string_view pluralsKey = "nplurals";
|
|
const string_view::size_type pluralsPos = string.find(pluralsKey);
|
|
if (pluralsPos == string_view::npos)
|
|
return;
|
|
string.remove_prefix(pluralsPos + pluralsKey.size());
|
|
|
|
const string_view::size_type eqPos = string.find('=');
|
|
if (eqPos == string_view::npos)
|
|
return;
|
|
|
|
string_view value = string.substr(eqPos + 1);
|
|
if (value.empty() || value[0] < '0')
|
|
return;
|
|
|
|
const unsigned nplurals = value[0] - '0';
|
|
if (nplurals == 0)
|
|
return;
|
|
|
|
PluralForms = nplurals;
|
|
|
|
SetPluralForm(value);
|
|
}
|
|
|
|
void ParseMetadata(string_view metadata)
|
|
{
|
|
string_view::size_type delim;
|
|
|
|
while (!metadata.empty() && ((delim = metadata.find(':')) != string_view::npos)) {
|
|
const string_view key = TrimLeft(string_view(metadata.data(), delim));
|
|
string_view val = TrimLeft(string_view(metadata.data() + delim + 1, metadata.size() - delim - 1));
|
|
|
|
if ((delim = val.find('\n')) != string_view::npos) {
|
|
val = string_view(val.data(), delim);
|
|
metadata.remove_prefix(val.data() - metadata.data() + val.size() + 1);
|
|
} else {
|
|
metadata.remove_prefix(metadata.size());
|
|
}
|
|
|
|
// Match "Plural-Forms: nplurals=2; plural=(n != 1);"
|
|
if (key == "Plural-Forms") {
|
|
ParsePluralForms(val);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ReadEntry(AssetHandle &handle, const MoEntry &e, char *result)
|
|
{
|
|
if (!handle.seek(e.offset))
|
|
return false;
|
|
result[e.length] = '\0';
|
|
return handle.read(result, e.length);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
string_view LanguageParticularTranslate(string_view context, string_view message)
|
|
{
|
|
constexpr const char Glue = '\004';
|
|
|
|
std::string key = std::string(context);
|
|
key.reserve(key.size() + 1 + message.size());
|
|
key += Glue;
|
|
AppendStrView(key, message);
|
|
|
|
auto it = translation[0].find(key.c_str());
|
|
if (it == translation[0].end()) {
|
|
return message;
|
|
}
|
|
|
|
return GetTranslation(it->second);
|
|
}
|
|
|
|
string_view LanguagePluralTranslate(const char *singular, string_view plural, int count)
|
|
{
|
|
int n = GetLocalPluralId(count);
|
|
|
|
auto it = translation[n].find(singular);
|
|
if (it == translation[n].end()) {
|
|
if (count != 1)
|
|
return plural;
|
|
return singular;
|
|
}
|
|
|
|
return GetTranslation(it->second);
|
|
}
|
|
|
|
string_view LanguageTranslate(const char *key)
|
|
{
|
|
auto it = translation[0].find(key);
|
|
if (it == translation[0].end()) {
|
|
return key;
|
|
}
|
|
|
|
return GetTranslation(it->second);
|
|
}
|
|
|
|
bool HasTranslation(const std::string &locale)
|
|
{
|
|
if (locale == "en") {
|
|
// the base translation is en (really en_US). No translation file will be present for this code but we want
|
|
// the check to succeed to prevent further searches.
|
|
return true;
|
|
}
|
|
|
|
constexpr std::array<const char *, 2> Extensions { ".mo", ".gmo" };
|
|
return std::any_of(Extensions.cbegin(), Extensions.cend(), [locale](const std::string &extension) {
|
|
return FindAsset((locale + extension).c_str()).ok();
|
|
});
|
|
}
|
|
|
|
bool IsSmallFontTall()
|
|
{
|
|
string_view code = (*sgOptions.Language.code).substr(0, 2);
|
|
return code == "zh" || code == "ja" || code == "ko";
|
|
}
|
|
|
|
void LanguageInitialize()
|
|
{
|
|
translation = { {}, {} };
|
|
translationKeys = nullptr;
|
|
translationValues = nullptr;
|
|
|
|
if (IsSmallFontTall() && !HaveExtraFonts()) {
|
|
UiErrorOkDialog(
|
|
"Missing fonts.mpq",
|
|
StrCat("fonts.mpq is required for locale \"",
|
|
*sgOptions.Language.code,
|
|
"\"\n\n"
|
|
"Please download fonts.mpq from:\n"
|
|
"github.com/diasurgical/\ndevilutionx-assets/releases"));
|
|
sgOptions.Language.code = "en";
|
|
}
|
|
|
|
const std::string lang(*sgOptions.Language.code);
|
|
|
|
AssetHandle handle;
|
|
|
|
// Translations normally come in ".gmo" files.
|
|
// We also support ".mo" because that is what poedit generates
|
|
// and what translators use to test their work.
|
|
for (const char *ext : { ".mo", ".gmo" }) {
|
|
if ((handle = OpenAsset((lang + ext).c_str())).ok()) {
|
|
break;
|
|
}
|
|
}
|
|
if (!handle.ok()) {
|
|
SetPluralForm("plural=(n != 1);"); // Reset to English plural form
|
|
return;
|
|
}
|
|
|
|
// Read header and do sanity checks
|
|
MoHead head;
|
|
if (!handle.read(&head, sizeof(MoHead))) {
|
|
return;
|
|
}
|
|
SwapLE(head);
|
|
|
|
if (head.magic != MO_MAGIC) {
|
|
return; // not a MO file
|
|
}
|
|
|
|
if (head.revision.major > 1 || head.revision.minor > 1) {
|
|
return; // unsupported revision
|
|
}
|
|
|
|
// Read entries of source strings
|
|
std::unique_ptr<MoEntry[]> src { new MoEntry[head.nbMappings] };
|
|
if (!handle.seek(head.srcOffset)) {
|
|
return;
|
|
}
|
|
if (!handle.read(src.get(), head.nbMappings * sizeof(MoEntry))) {
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < head.nbMappings; ++i) {
|
|
SwapLE(src[i]);
|
|
}
|
|
|
|
// Read entries of target strings
|
|
std::unique_ptr<MoEntry[]> dst { new MoEntry[head.nbMappings] };
|
|
if (!handle.seek(head.dstOffset)) {
|
|
return;
|
|
}
|
|
if (!handle.read(dst.get(), head.nbMappings * sizeof(MoEntry))) {
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < head.nbMappings; ++i) {
|
|
SwapLE(dst[i]);
|
|
}
|
|
|
|
// MO header
|
|
if (src[0].length != 0) {
|
|
return;
|
|
}
|
|
{
|
|
auto headerValue = std::unique_ptr<char[]> { new char[dst[0].length + 1] };
|
|
if (!ReadEntry(handle, dst[0], &headerValue[0])) {
|
|
return;
|
|
}
|
|
ParseMetadata(&headerValue[0]);
|
|
}
|
|
|
|
translation.resize(PluralForms);
|
|
for (unsigned i = 0; i < PluralForms; i++)
|
|
translation[i] = {};
|
|
|
|
// Read strings described by entries
|
|
size_t keysSize = 0;
|
|
size_t valuesSize = 0;
|
|
for (uint32_t i = 1; i < head.nbMappings; i++) {
|
|
keysSize += src[i].length + 1;
|
|
valuesSize += dst[i].length + 1;
|
|
}
|
|
translationKeys = std::unique_ptr<char[]> { new char[keysSize] };
|
|
translationValues = std::unique_ptr<char[]> { new char[valuesSize] };
|
|
|
|
char *keyPtr = &translationKeys[0];
|
|
char *valuePtr = &translationValues[0];
|
|
for (uint32_t i = 1; i < head.nbMappings; i++) {
|
|
if (ReadEntry(handle, src[i], keyPtr) && ReadEntry(handle, dst[i], valuePtr)) {
|
|
// Plural keys also have a plural form but it does not participate in lookup.
|
|
// Plural values are \0-terminated.
|
|
string_view value { valuePtr, dst[i].length + 1 };
|
|
for (size_t j = 0; j < PluralForms && !value.empty(); j++) {
|
|
const size_t formValueEnd = value.find('\0');
|
|
translation[j].emplace(keyPtr, EncodeTranslationRef(value.data() - &translationValues[0], formValueEnd));
|
|
value.remove_prefix(formValueEnd + 1);
|
|
}
|
|
|
|
keyPtr += src[i].length + 1;
|
|
valuePtr += dst[i].length + 1;
|
|
}
|
|
}
|
|
}
|