refactor(i18n): use the shared JSON reader
I18n.cpp carried the original copy of the recursive-descent parser that common/Json.h now hosts. Drop the 228 local lines and flatten the parsed tree through eve::json::Value instead. Locale flattening keeps its behaviour: nested keys join with dots, an object whose members are all plural categories becomes a plural table, and scalars stringify. Arrays and nulls were already skipped and still are, now stated directly rather than implied by the if/else chain. Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
This commit is contained in:
1 file changed
+17
-246
+17
-246
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "filesystem/FileData.h"
|
||||
#include "filesystem/Filesystem.h"
|
||||
#include "common/Json.h"
|
||||
#include "common/Module.h"
|
||||
|
||||
#include <simplesquirrel/simplesquirrel.hpp>
|
||||
@@ -17,234 +18,8 @@ namespace eve::i18n {
|
||||
|
||||
Module_IMPL(I18n, new I18n());
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Minimal recursive-descent JSON parser (module-local; no Poco dependency so
|
||||
// this module also builds on the trimmed WASM target).
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
struct JsonValue {
|
||||
enum class Kind { Null, Bool, Number, String, Object, Array };
|
||||
|
||||
Kind kind = Kind::Null;
|
||||
bool boolVal = false;
|
||||
double numberVal = 0.0;
|
||||
std::string stringVal;
|
||||
std::vector<std::pair<std::string, JsonValue>> object; // ordered
|
||||
std::vector<JsonValue> array;
|
||||
};
|
||||
|
||||
class JsonParser {
|
||||
public:
|
||||
explicit JsonParser(const std::string &text) : s_(text) {}
|
||||
|
||||
bool parse(JsonValue &out, std::string *error) {
|
||||
skipWs();
|
||||
if (!parseValue(out)) {
|
||||
if (error) *error = "invalid JSON near offset " + std::to_string(pos_);
|
||||
return false;
|
||||
}
|
||||
skipWs();
|
||||
if (pos_ != s_.size()) {
|
||||
if (error) *error = "trailing data at offset " + std::to_string(pos_);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string &s_;
|
||||
size_t pos_ = 0;
|
||||
|
||||
void skipWs() {
|
||||
while (pos_ < s_.size() && (s_[pos_] == ' ' || s_[pos_] == '\t' || s_[pos_] == '\n' ||
|
||||
s_[pos_] == '\r'))
|
||||
++pos_;
|
||||
}
|
||||
|
||||
bool peek(char c) const { return pos_ < s_.size() && s_[pos_] == c; }
|
||||
|
||||
bool parseValue(JsonValue &out) {
|
||||
if (pos_ >= s_.size()) return false;
|
||||
switch (s_[pos_]) {
|
||||
case '{': return parseObject(out);
|
||||
case '[': return parseArray(out);
|
||||
case '"': return parseString(out.stringVal) ? (out.kind = JsonValue::Kind::String, true) : false;
|
||||
case 't': return parseLiteral("true", out, true);
|
||||
case 'f': return parseLiteral("false", out, false);
|
||||
case 'n': return parseNull(out);
|
||||
default: return parseNumber(out);
|
||||
}
|
||||
}
|
||||
|
||||
bool parseLiteral(const char *lit, JsonValue &out, bool value) {
|
||||
const size_t n = std::char_traits<char>::length(lit);
|
||||
if (s_.compare(pos_, n, lit) != 0) return false;
|
||||
pos_ += n;
|
||||
out.kind = JsonValue::Kind::Bool;
|
||||
out.boolVal = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseNull(JsonValue &out) {
|
||||
if (s_.compare(pos_, 4, "null") != 0) return false;
|
||||
pos_ += 4;
|
||||
out.kind = JsonValue::Kind::Null;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseNumber(JsonValue &out) {
|
||||
const size_t start = pos_;
|
||||
if (pos_ < s_.size() && (s_[pos_] == '-' || s_[pos_] == '+')) ++pos_;
|
||||
bool hasDigit = false;
|
||||
while (pos_ < s_.size() && s_[pos_] >= '0' && s_[pos_] <= '9') {
|
||||
++pos_;
|
||||
hasDigit = true;
|
||||
}
|
||||
if (pos_ < s_.size() && s_[pos_] == '.') {
|
||||
++pos_;
|
||||
while (pos_ < s_.size() && s_[pos_] >= '0' && s_[pos_] <= '9') ++pos_;
|
||||
}
|
||||
if (pos_ < s_.size() && (s_[pos_] == 'e' || s_[pos_] == 'E')) {
|
||||
++pos_;
|
||||
if (pos_ < s_.size() && (s_[pos_] == '-' || s_[pos_] == '+')) ++pos_;
|
||||
while (pos_ < s_.size() && s_[pos_] >= '0' && s_[pos_] <= '9') ++pos_;
|
||||
}
|
||||
if (!hasDigit) return false;
|
||||
std::string num = s_.substr(start, pos_ - start);
|
||||
try {
|
||||
out.numberVal = std::stod(num);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
out.kind = JsonValue::Kind::Number;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseString(std::string &out) {
|
||||
if (!peek('"')) return false;
|
||||
++pos_;
|
||||
out.clear();
|
||||
while (pos_ < s_.size()) {
|
||||
const char c = s_[pos_++];
|
||||
if (c == '"') return true;
|
||||
if (c == '\\') {
|
||||
if (pos_ >= s_.size()) return false;
|
||||
const char esc = s_[pos_++];
|
||||
switch (esc) {
|
||||
case '"': out += '"'; break;
|
||||
case '\\': out += '\\'; break;
|
||||
case '/': out += '/'; break;
|
||||
case 'b': out += '\b'; break;
|
||||
case 'f': out += '\f'; break;
|
||||
case 'n': out += '\n'; break;
|
||||
case 'r': out += '\r'; break;
|
||||
case 't': out += '\t'; break;
|
||||
case 'u': {
|
||||
if (pos_ + 4 > s_.size()) return false;
|
||||
const char hex[5] = {s_[pos_], s_[pos_ + 1], s_[pos_ + 2], s_[pos_ + 3], '\0'};
|
||||
pos_ += 4;
|
||||
char *end = nullptr;
|
||||
const unsigned cp = static_cast<unsigned>(std::strtoul(hex, &end, 16));
|
||||
if (!end || *end != '\0') return false;
|
||||
if (cp >= 0xD800 && cp <= 0xDBFF && pos_ + 6 <= s_.size() &&
|
||||
s_[pos_] == '\\' && s_[pos_ + 1] == 'u') {
|
||||
const char hex2[5] = {s_[pos_ + 2], s_[pos_ + 3], s_[pos_ + 4],
|
||||
s_[pos_ + 5], '\0'};
|
||||
pos_ += 6;
|
||||
char *end2 = nullptr;
|
||||
const unsigned lo = static_cast<unsigned>(std::strtoul(hex2, &end2, 16));
|
||||
if (end2 && *end2 == '\0' && lo >= 0xDC00 && lo <= 0xDFFF)
|
||||
appendUtf8(out, 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00));
|
||||
else
|
||||
appendUtf8(out, cp);
|
||||
} else {
|
||||
appendUtf8(out, cp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
return false; // unterminated
|
||||
}
|
||||
|
||||
static void appendUtf8(std::string &out, unsigned cp) {
|
||||
if (cp < 0x80) {
|
||||
out += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
out += static_cast<char>(0xC0 | (cp >> 6));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
out += static_cast<char>(0xE0 | (cp >> 12));
|
||||
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
out += static_cast<char>(0xF0 | (cp >> 18));
|
||||
out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
bool parseObject(JsonValue &out) {
|
||||
++pos_; // '{'
|
||||
skipWs();
|
||||
if (peek('}')) {
|
||||
++pos_;
|
||||
out.kind = JsonValue::Kind::Object;
|
||||
return true;
|
||||
}
|
||||
while (true) {
|
||||
skipWs();
|
||||
std::string key;
|
||||
if (!parseString(key)) return false;
|
||||
skipWs();
|
||||
if (!peek(':')) return false;
|
||||
++pos_;
|
||||
skipWs();
|
||||
JsonValue val;
|
||||
if (!parseValue(val)) return false;
|
||||
out.object.emplace_back(std::move(key), std::move(val));
|
||||
skipWs();
|
||||
if (peek('}')) {
|
||||
++pos_;
|
||||
out.kind = JsonValue::Kind::Object;
|
||||
return true;
|
||||
}
|
||||
if (!peek(',')) return false;
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
|
||||
bool parseArray(JsonValue &out) {
|
||||
++pos_; // '['
|
||||
skipWs();
|
||||
if (peek(']')) {
|
||||
++pos_;
|
||||
out.kind = JsonValue::Kind::Array;
|
||||
return true;
|
||||
}
|
||||
while (true) {
|
||||
skipWs();
|
||||
JsonValue val;
|
||||
if (!parseValue(val)) return false;
|
||||
out.array.push_back(std::move(val));
|
||||
skipWs();
|
||||
if (peek(']')) {
|
||||
++pos_;
|
||||
out.kind = JsonValue::Kind::Array;
|
||||
return true;
|
||||
}
|
||||
if (!peek(',')) return false;
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool isPluralCategory(const std::string &key) {
|
||||
static const char *cats[] = {"zero", "one", "two", "few", "many", "other"};
|
||||
for (const char *c : cats)
|
||||
@@ -252,62 +27,58 @@ bool isPluralCategory(const std::string &key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Default ostream formatting, so script floats interpolate like JSON numbers. */
|
||||
std::string numberToString(double v) {
|
||||
std::ostringstream os;
|
||||
os << v;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
void flatten(const JsonValue &node, const std::string &prefix,
|
||||
void flatten(eve::json::Value node, const std::string &prefix,
|
||||
std::unordered_map<std::string, std::string> &strings,
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> &plurals) {
|
||||
if (node.kind == JsonValue::Kind::Object) {
|
||||
if (node.isObject()) {
|
||||
const std::vector<std::string> names = node.keys();
|
||||
// A plain object of plural categories is a plural form table.
|
||||
bool allCats = !node.object.empty();
|
||||
bool allCats = !names.empty();
|
||||
bool allStrings = true;
|
||||
for (const auto &[k, v] : node.object) {
|
||||
for (const auto &k : names) {
|
||||
if (!isPluralCategory(k)) {
|
||||
allCats = false;
|
||||
break;
|
||||
}
|
||||
if (v.kind != JsonValue::Kind::String) allStrings = false;
|
||||
if (!node.get(k.c_str()).isString()) allStrings = false;
|
||||
}
|
||||
if (allCats && allStrings && !prefix.empty()) {
|
||||
std::unordered_map<std::string, std::string> forms;
|
||||
for (const auto &[k, v] : node.object) forms[k] = v.stringVal;
|
||||
for (const auto &k : names) forms[k] = node.getString(k.c_str());
|
||||
plurals[prefix] = std::move(forms);
|
||||
return;
|
||||
}
|
||||
for (const auto &[k, v] : node.object) {
|
||||
for (const auto &k : names) {
|
||||
const std::string path = prefix.empty() ? k : prefix + "." + k;
|
||||
flatten(v, path, strings, plurals);
|
||||
flatten(node.get(k.c_str()), path, strings, plurals);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (prefix.empty()) return;
|
||||
if (node.kind == JsonValue::Kind::String) {
|
||||
strings[prefix] = node.stringVal;
|
||||
} else if (node.kind == JsonValue::Kind::Number) {
|
||||
strings[prefix] = numberToString(node.numberVal);
|
||||
} else if (node.kind == JsonValue::Kind::Bool) {
|
||||
strings[prefix] = node.boolVal ? "true" : "false";
|
||||
}
|
||||
// Scalars stringify; arrays and nulls are not translatable and are skipped.
|
||||
if (node.isString() || node.isNumber() || node.isBool()) strings[prefix] = node.asString();
|
||||
}
|
||||
|
||||
bool parseLocale(const std::string &text,
|
||||
std::unordered_map<std::string, std::string> &strings,
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> &plurals,
|
||||
std::string *error) {
|
||||
JsonValue root;
|
||||
JsonParser parser(text);
|
||||
if (!parser.parse(root, error)) return false;
|
||||
if (root.kind != JsonValue::Kind::Object) {
|
||||
const eve::json::Document doc = eve::json::Document::parse(text, error);
|
||||
if (!doc.valid()) return false;
|
||||
if (!doc.root().isObject()) {
|
||||
if (error) *error = "locale root must be a JSON object";
|
||||
return false;
|
||||
}
|
||||
strings.clear();
|
||||
plurals.clear();
|
||||
flatten(root, "", strings, plurals);
|
||||
flatten(doc.root(), "", strings, plurals);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user