devilutionX/Source/utils/language.cpp
Gleb Mazovetskiy 4f64b87330 Fix talk/store text overlap for Chinese and Japanese
We ensure that selectable lines are placed at the same vertical
coordinates but space out unselectable text lines at the cost
of reduced heigh of empty space between the store items.

We also have to move the back button in scrollable lists to the
lower right.

This can definitely be improved further but at least it solves
the problem for now.

Refs #3162
2021-11-20 02:31:21 +00:00

383 lines
8.1 KiB
C++

#include "utils/language.h"
#include <functional>
#include <map>
#include <memory>
#include <vector>
#include "options.h"
#include "engine/assets.hpp"
#include "utils/file_util.h"
#include "utils/paths.h"
#include "utils/stdcompat/string_view.hpp"
using namespace devilution;
#define MO_MAGIC 0x950412de
namespace {
struct CStringCmp {
bool operator()(const char *s1, const char *s2) const
{
return strcmp(s1, s2) < 0;
}
};
std::vector<std::map<std::string, std::string, std::less<>>> translation = { {}, {} };
struct MoHead {
uint32_t magic;
struct {
uint16_t major;
uint16_t minor;
} revision;
uint32_t nbMappings;
uint32_t srcOffset;
uint32_t dstOffset;
};
struct MoEntry {
uint32_t length;
uint32_t offset;
};
char *StrTrimLeft(char *s)
{
while (*s != '\0' && isblank(*s) != 0) {
s++;
}
return s;
}
char *StrTrimRight(char *s)
{
size_t length = strlen(s);
while (length != 0) {
length--;
if (isblank(s[length]) != 0) {
s[length] = '\0';
} else {
break;
}
}
return s;
}
// English, Danish, Spanish, Italian, Swedish
int PluralForms = 2;
std::function<int(int n)> GetLocalPluralId = [](int n) -> int { return n != 1 ? 1 : 0; };
/**
* Match plural=(n != 1);"
*/
void SetPluralForm(char *string)
{
char *expression = strstr(string, "plural");
if (expression == nullptr)
return;
expression = strstr(expression, "=");
if (expression == nullptr)
return;
expression += 1;
for (unsigned i = 0; i < strlen(expression); i++) {
if (expression[i] == ';') {
expression[i] = '\0';
break;
}
}
expression = StrTrimRight(expression);
expression = StrTrimLeft(expression);
// ko_KR, zh_CN, zh_TW
if (strcmp(expression, "0") == 0) {
GetLocalPluralId = [](int /*n*/) -> int { return 0; };
return;
}
// fr, pt_BR
if (strcmp(expression, "(n > 1)") == 0) {
GetLocalPluralId = [](int n) -> int { return n > 1 ? 1 : 0; };
return;
}
// hr, ru
if (strcmp(expression, "(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)") == 0) {
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 (strcmp(expression, "(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)") == 0) {
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 (strcmp(expression, "(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2)") == 0) {
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 (strcmp(expression, "(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2") == 0) {
GetLocalPluralId = [](int n) -> int {
if (n == 1)
return 0;
if (n >= 2 && n <= 4)
return 1;
return 2;
};
return;
}
// bg, da, de, es, it, sv
// (n != 1)
}
/**
* Parse "nplurals=2;"
*/
void ParsePluralForms(char *string)
{
char *value = strstr(string, "nplurals");
if (value == nullptr)
return;
value = strstr(value, "=");
if (value == nullptr)
return;
value += 1;
int nplurals = SDL_atoi(value);
if (nplurals == 0)
return;
PluralForms = nplurals;
SetPluralForm(value);
}
void ParseMetadata(char *ptr)
{
char *delim;
while ((ptr != nullptr) && ((delim = strstr(ptr, ":")) != nullptr)) {
char *key = StrTrimLeft(ptr);
char *val = StrTrimLeft(delim + 1);
// null-terminate key
*delim = '\0';
// progress to next line (if any)
if ((ptr = strstr(val, "\n")) != nullptr) {
*ptr = '\0';
ptr++;
}
val = StrTrimRight(val);
if ((strcmp("Content-Type", key) == 0) && ((delim = strstr(val, "=")) != nullptr)) {
if (strcasecmp(delim + 1, "utf-8") != 0) {
Log("Translation is now UTF-8 encoded!");
}
continue;
}
// Match "Plural-Forms: nplurals=2; plural=(n != 1);"
if (strcmp("Plural-Forms", key) == 0) {
ParsePluralForms(val);
continue;
}
}
}
bool ReadEntry(SDL_RWops *rw, MoEntry *e, std::vector<char> &result)
{
if (SDL_RWseek(rw, e->offset, RW_SEEK_SET) == -1)
return false;
result.resize(e->length + 1);
result.back() = '\0';
return (SDL_RWread(rw, result.data(), sizeof(char), e->length) == e->length);
}
} // namespace
const std::string &LanguageParticularTranslate(const char *context, const char *message)
{
constexpr const char *glue = "\004";
std::string key = context;
key += glue;
key += message;
auto it = translation[0].find(key);
if (it == translation[0].end()) {
it = translation[0].insert({ key, message }).first;
}
return it->second;
}
const std::string &LanguagePluralTranslate(const char *singular, const char *plural, int count)
{
int n = GetLocalPluralId(count);
auto it = translation[n].find(singular);
if (it == translation[n].end()) {
if (count != 1)
it = translation[1].insert({ singular, plural }).first;
else
it = translation[0].insert({ singular, singular }).first;
}
return it->second;
}
const std::string &LanguageTranslate(const char *key)
{
auto it = translation[0].find(key);
if (it == translation[0].end()) {
it = translation[0].insert({ key, key }).first;
}
return it->second;
}
bool HasTranslation(const std::string &locale)
{
for (const char *ext : { ".mo", ".gmo" }) {
SDL_RWops *rw = OpenAsset((locale + ext).c_str());
if (rw != nullptr) {
SDL_RWclose(rw);
return true;
}
}
return false;
}
bool IsSmallFontTall()
{
string_view code(sgOptions.Language.szCode, 2);
return code == "zh" || code == "ja" || code == "ko";
}
void LanguageInitialize()
{
const std::string lang = sgOptions.Language.szCode;
SDL_RWops *rw;
// 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 ((rw = OpenAsset((lang + ext).c_str())) != nullptr)
break;
}
if (rw == nullptr)
return;
// Read header and do sanity checks
// FIXME: Endianness.
MoHead head;
if (SDL_RWread(rw, &head, sizeof(MoHead), 1) != 1) {
SDL_RWclose(rw);
return;
}
if (head.magic != MO_MAGIC) {
SDL_RWclose(rw);
return; // not a MO file
}
if (head.revision.major > 1 || head.revision.minor > 1) {
SDL_RWclose(rw);
return; // unsupported revision
}
// Read entries of source strings
std::unique_ptr<MoEntry[]> src { new MoEntry[head.nbMappings] };
if (SDL_RWseek(rw, head.srcOffset, RW_SEEK_SET) == -1) {
SDL_RWclose(rw);
return;
}
// FIXME: Endianness.
if (SDL_RWread(rw, src.get(), sizeof(MoEntry), head.nbMappings) != head.nbMappings) {
SDL_RWclose(rw);
return;
}
// Read entries of target strings
std::unique_ptr<MoEntry[]> dst { new MoEntry[head.nbMappings] };
if (SDL_RWseek(rw, head.dstOffset, RW_SEEK_SET) == -1) {
SDL_RWclose(rw);
return;
}
// FIXME: Endianness.
if (SDL_RWread(rw, dst.get(), sizeof(MoEntry), head.nbMappings) != head.nbMappings) {
SDL_RWclose(rw);
return;
}
std::vector<char> key;
std::vector<char> value;
// MO header
if (!ReadEntry(rw, &src[0], key) || !ReadEntry(rw, &dst[0], value)) {
SDL_RWclose(rw);
return;
}
if (key[0] != '\0') {
SDL_RWclose(rw);
return;
}
ParseMetadata(value.data());
translation.resize(PluralForms);
for (int i = 0; i < PluralForms; i++)
translation[i] = {};
// Read strings described by entries
for (uint32_t i = 1; i < head.nbMappings; i++) {
if (ReadEntry(rw, &src[i], key) && ReadEntry(rw, &dst[i], value)) {
size_t offset = 0;
for (int j = 0; j < PluralForms; j++) {
const char *text = value.data() + offset;
translation[j].emplace(key.data(), text);
if (dst[i].length <= offset + strlen(value.data()))
break;
offset += strlen(text) + 1;
}
}
}
SDL_RWclose(rw);
}