Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ if(BUILD_CAPI)
dasher_add_test(dasher_deterministic_tests test_deterministic.cpp)
dasher_add_test(dasher_training_tests test_training.cpp)
dasher_add_test(dasher_node_tree_tests test_node_tree.cpp)
dasher_add_test(dasher_locales_sync_tests test_locales_sync.cpp)

# Phase B (characterization) — closes coverage gaps noted in the review.
# - buffer_lifetime: the C API "valid until next call" contract
Expand Down
170 changes: 170 additions & 0 deletions Strings/locales.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{
"_comment": "Canonical list of locales DasherCore supports. Mirrors the strings_*.json files in this directory — the single source of truth for every frontend's locale picker (RFC 0003). Display ordering is the frontend's concern. Regenerate with the table in RFC 0003; keep in sync with strings_*.json.",
"locales": [
{
"code": "af",
"endonym": "Afrikaans",
"rtl": false
},
{
"code": "ar",
"endonym": "العربية",
"rtl": true
},
{
"code": "bn",
"endonym": "বাংলা",
"rtl": false
},
{
"code": "cs",
"endonym": "Čeština",
"rtl": false
},
{
"code": "da",
"endonym": "Dansk",
"rtl": false
},
{
"code": "de",
"endonym": "Deutsch",
"rtl": false
},
{
"code": "el",
"endonym": "Ελληνικά",
"rtl": false
},
{
"code": "en",
"endonym": "English",
"rtl": false
},
{
"code": "es",
"endonym": "Español",
"rtl": false
},
{
"code": "fa",
"endonym": "فارسی",
"rtl": true
},
{
"code": "fi",
"endonym": "Suomi",
"rtl": false
},
{
"code": "fr",
"endonym": "Français",
"rtl": false
},
{
"code": "gu",
"endonym": "ગુજરાતી",
"rtl": false
},
{
"code": "hi",
"endonym": "हिन्दी",
"rtl": false
},
{
"code": "hu",
"endonym": "Magyar",
"rtl": false
},
{
"code": "it",
"endonym": "Italiano",
"rtl": false
},
{
"code": "kn",
"endonym": "ಕನ್ನಡ",
"rtl": false
},
{
"code": "ml",
"endonym": "മലയാളം",
"rtl": false
},
{
"code": "mr",
"endonym": "मराठी",
"rtl": false
},
{
"code": "nl",
"endonym": "Nederlands",
"rtl": false
},
{
"code": "pa",
"endonym": "ਪੰਜਾਬੀ",
"rtl": false
},
{
"code": "pl",
"endonym": "Polski",
"rtl": false
},
{
"code": "pt",
"endonym": "Português (Brasil)",
"rtl": false
},
{
"code": "pt-PT",
"endonym": "Português (Portugal)",
"rtl": false
},
{
"code": "ru",
"endonym": "Русский",
"rtl": false
},
{
"code": "sv",
"endonym": "Svenska",
"rtl": false
},
{
"code": "sw",
"endonym": "Kiswahili",
"rtl": false
},
{
"code": "ta",
"endonym": "தமிழ்",
"rtl": false
},
{
"code": "te",
"endonym": "తెలుగు",
"rtl": false
},
{
"code": "th",
"endonym": "ไทย",
"rtl": false
},
{
"code": "ur",
"endonym": "اردو",
"rtl": true
},
{
"code": "zh-CN",
"endonym": "中文(简体)",
"rtl": false
},
{
"code": "zu",
"endonym": "isiZulu",
"rtl": false
}
]
}
83 changes: 83 additions & 0 deletions tests/test_locales_sync.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// test_locales_sync.cpp — guard that Strings/locales.json (the canonical locale
// list, RFC 0003) stays in lock-step with the strings_*.json files actually
// shipped. Prevents silent drift: add a strings_xx.json but forget locales.json
// (or vice versa) and this test fails.
//
// Self-contained: no JSON dependency. Codes are pulled from the
// strings_<code>.json filenames and from a focused scan of locales.json (a
// generated, controlled file whose "code" keys are the only `"code":` matches).

#include "test_common.h"

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <set>
#include <sstream>
#include <string>

namespace fs = std::filesystem;

static fs::path strings_dir() {
return fs::path(get_test_data_dir()) / "Strings";
}

// Codes implied by the shipped strings_<code>.json files.
static std::set<std::string> shipped_codes() {
std::set<std::string> codes;
for (const auto& entry : fs::directory_iterator(strings_dir())) {
const std::string name = entry.path().filename().string();
const std::string pfx = "strings_";
const std::string ext = ".json";
if (name.rfind(pfx, 0) == 0 && name.size() > pfx.size() + ext.size() &&
name.compare(name.size() - ext.size(), ext.size(), ext) == 0) {
codes.insert(name.substr(pfx.size(), name.size() - pfx.size() - ext.size()));
}
}
return codes;
}

// Codes listed in locales.json, by scanning for `"code": "<value>"`.
static std::set<std::string> listed_codes() {
const fs::path file = strings_dir() / "locales.json";
std::ifstream in(file);
INFO("could not open " << file.string());
REQUIRE(in.good());
std::stringstream ss;
ss << in.rdbuf();
const std::string text = ss.str();
const std::string needle = "\"code\":";
std::set<std::string> codes;
size_t pos = 0;
while ((pos = text.find(needle, pos)) != std::string::npos) {
pos += needle.size();
const size_t q1 = text.find('"', pos);
if (q1 == std::string::npos) break;
const size_t q2 = text.find('"', q1 + 1);
if (q2 == std::string::npos) break;
codes.insert(text.substr(q1 + 1, q2 - q1 - 1));
pos = q2 + 1;
}
return codes;
}

TEST_CASE("locales.json matches the shipped strings_*.json files") {
const std::set<std::string> shipped = shipped_codes();
const std::set<std::string> listed = listed_codes();

REQUIRE_FALSE(shipped.empty());
MESSAGE("shipped=" << shipped.size() << " listed=" << listed.size());

std::set<std::string> missing; // shipped but not listed
std::set<std::string> extra; // listed but not shipped
std::set_difference(shipped.begin(), shipped.end(), listed.begin(), listed.end(),
std::inserter(missing, missing.begin()));
std::set_difference(listed.begin(), listed.end(), shipped.begin(), shipped.end(),
std::inserter(extra, extra.begin()));

INFO("locales.json missing codes that have a strings_*.json file (missing=" << missing.size() << ")");
REQUIRE(missing.empty());
INFO("locales.json lists codes with no strings_*.json file (extra=" << extra.size() << ")");
REQUIRE(extra.empty());
}
Loading