Skip to content

Commit bd04e98

Browse files
committed
feat(vxcore): make month/day snippet names follow an app-set locale
The built-in %MMM%/%MMMM%/%ddd%/%dddd% snippets resolved day and month names through std::strftime, i.e. the process-wide C runtime LC_TIME. That is the wrong source of truth (it tracks the OS locale, not the embedder's language setting) and, on Windows with a non-C LC_TIME, it emits ANSI code page bytes that become mojibake once they cross the C ABI into a UTF-8-decoding consumer. Add locale-independent UTF-8 name tables (en, zh_CN, ja) in core/datetime_names, a runtime-only context locale (vxcore_context_set_locale / _get_locale, purely additive ABI), and have the four callbacks read the table at call time. English is the deterministic default, so the CLI, tests and other embedders no longer depend on the host locale. Non-ASCII entries use explicit UTF-8 byte escapes because the production target is not built with /utf-8. SnippetManager's copy and move operations are deleted: the dynamic callbacks now capture this.
1 parent c7e31a1 commit bd04e98

11 files changed

Lines changed: 543 additions & 16 deletions

File tree

AGENTS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,31 @@ VXCORE_LOG_INFO("Opening notebook: %s", path.c_str());
577577
VXCORE_LOG_ERROR("Failed to open database: %s", db_path.c_str());
578578
```
579579

580+
### Context locale
581+
582+
`vxcore_context_set_locale(ctx, "zh_CN")` / `vxcore_context_get_locale(ctx, &out)` set the
583+
app-wide locale used for locale-aware, UTF-8 output. Today the only consumer is
584+
`SnippetManager`'s built-in `%MMM%` / `%MMMM%` / `%ddd%` / `%dddd%` snippets.
585+
586+
- **Runtime-only.** The locale is consumer *policy* pushed in at startup; it is NEVER written
587+
to `vxcore.json`. (VNote pushes `QLocale().name()` right after `loadTranslators`.)
588+
- **English is the deterministic default.** A context that is never told a locale resolves
589+
through the English table — output does not depend on the host OS locale, so the CLI, tests
590+
and other embedders are reproducible.
591+
- **Month/day names never come from `strftime` / `setlocale`.** They come from the
592+
statically-compiled UTF-8 tables in `src/core/datetime_names.{h,cpp}`. `strftime` would
593+
return ANSI-code-page bytes on Windows, which become mojibake once they cross the C ABI
594+
into a UTF-8-decoding consumer. Do not reintroduce a `strftime`-based helper.
595+
- **Non-ASCII table entries are written as explicit UTF-8 byte escapes** (`"\xE5\x9B\x9B..."`).
596+
The production `vxcore` target does not get MSVC's `/utf-8`, so a raw CJK literal would be
597+
re-encoded with the active ANSI code page. Do not "clean them up".
598+
- **Adding a locale** means adding a table entry in `datetime_names.cpp` plus a
599+
`CanonicalizeLocale` mapping — nothing else. Matching is: normalize `-``_`, lowercase,
600+
exact match, then language subtag, else English.
601+
- `SnippetManager` is not thread-safe; `SetLocale` inherits that. Set it before use, from the
602+
thread that applies snippets. Its copy/move operations are deleted because the built-in
603+
dynamic callbacks capture `this`.
604+
580605
## Common Patterns
581606

582607
### Adding a New C API Function

include/vxcore/vxcore.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ VXCORE_API VxCoreError vxcore_context_create(const char *config_json,
2828

2929
VXCORE_API void vxcore_context_destroy(VxCoreContextHandle context);
3030

31+
// Set the app-wide locale used for locale-aware, UTF-8 output (currently the
32+
// built-in %MMM%/%MMMM%/%ddd%/%dddd% snippets). |locale| is a Qt/POSIX-style name
33+
// such as "en_US", "zh_CN", "ja". Unknown or NULL/empty resolves to English.
34+
// Runtime-only: NOT persisted to vxcore.json. Not thread-safe; call before use.
35+
VXCORE_API VxCoreError vxcore_context_set_locale(VxCoreContextHandle context,
36+
const char *locale);
37+
38+
// Out: canonical locale name ("en" | "zh_CN" | "ja"). Free with vxcore_string_free.
39+
VXCORE_API VxCoreError vxcore_context_get_locale(VxCoreContextHandle context, char **out_locale);
40+
3141
// Snapshot current session state (buffers, workspaces) to disk.
3242
// Sets the shutdown_called flag, preventing destructor double-saves.
3343
// Idempotent: no-op if already called.

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ set(VXCORE_SOURCES
4747
core/raw_folder_manager.cpp
4848
core/template_manager.cpp
4949
core/snippet_manager.cpp
50+
core/datetime_names.cpp
5051
core/work_queue.cpp
5152
core/event_manager.cpp
5253
core/activity_manager.cpp

src/api/vxcore_api.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "core/buffer_manager.h"
66
#include "core/config_manager.h"
77
#include "core/context.h"
8+
#include "core/datetime_names.h"
89
#include "core/event_manager.h"
910
#include "core/notebook_manager.h"
1011
#include "core/snippet_manager.h"
@@ -277,6 +278,46 @@ VXCORE_API VxCoreError vxcore_context_get_last_error(VxCoreContextHandle context
277278
return VXCORE_OK;
278279
}
279280

281+
VXCORE_API VxCoreError vxcore_context_set_locale(VxCoreContextHandle context,
282+
const char *locale) {
283+
if (!context) {
284+
return VXCORE_ERR_NULL_POINTER;
285+
}
286+
287+
auto *ctx = reinterpret_cast<vxcore::VxCoreContext *>(context);
288+
try {
289+
// NULL/empty is valid and means English.
290+
ctx->locale = vxcore::CanonicalizeLocale(locale ? std::string(locale) : std::string());
291+
if (ctx->snippet_manager) {
292+
ctx->snippet_manager->SetLocale(ctx->locale);
293+
}
294+
return VXCORE_OK;
295+
} catch (const std::exception &e) {
296+
VXCORE_LOG_ERROR("vxcore_context_set_locale failed: %s", e.what());
297+
return VXCORE_ERR_UNKNOWN;
298+
}
299+
}
300+
301+
VXCORE_API VxCoreError vxcore_context_get_locale(VxCoreContextHandle context, char **out_locale) {
302+
if (!context || !out_locale) {
303+
return VXCORE_ERR_NULL_POINTER;
304+
}
305+
306+
*out_locale = nullptr;
307+
308+
auto *ctx = reinterpret_cast<vxcore::VxCoreContext *>(context);
309+
try {
310+
*out_locale = vxcore_strdup(ctx->locale.c_str());
311+
if (!*out_locale) {
312+
return VXCORE_ERR_OUT_OF_MEMORY;
313+
}
314+
return VXCORE_OK;
315+
} catch (const std::exception &e) {
316+
VXCORE_LOG_ERROR("vxcore_context_get_locale failed: %s", e.what());
317+
return VXCORE_ERR_UNKNOWN;
318+
}
319+
}
320+
280321
VXCORE_API VxCoreError vxcore_context_get_data_path(VxCoreContextHandle context,
281322
VxCoreDataLocation location, char **out_path) {
282323
if (!context || !out_path) {

src/core/context.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ struct VxCoreContext {
3737
// be alive at that point.
3838
std::unique_ptr<ActivityManager> activity_manager;
3939
std::string last_error;
40+
// App-wide locale used for locale-aware, UTF-8 output (see
41+
// vxcore_context_set_locale). Runtime-only: never persisted to vxcore.json.
42+
std::string locale = "en";
4043
bool shutdown_called = false;
4144
};
4245

src/core/datetime_names.cpp

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#include "core/datetime_names.h"
2+
3+
#include <cctype>
4+
5+
namespace vxcore {
6+
7+
// NOTE: every non-ASCII entry below is written as explicit UTF-8 byte escapes.
8+
// The production `vxcore` target is NOT compiled with MSVC's /utf-8 flag (that
9+
// flag is applied directory-wide only under libs/vxcore/tests), so a raw CJK
10+
// literal here would be re-encoded using the active ANSI code page in the DLL
11+
// while the direct-compiled test copy compiled correctly. The escapes remove
12+
// the source-encoding dependence entirely. Do NOT "clean them up" into raw
13+
// literals, and keep the comments in this file ASCII-only.
14+
namespace {
15+
16+
// ---------------------------------------------------------------------------
17+
// English
18+
// ---------------------------------------------------------------------------
19+
const char *const kEnShortMonths[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
20+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
21+
const char *const kEnLongMonths[12] = {"January", "February", "March", "April",
22+
"May", "June", "July", "August",
23+
"September", "October", "November", "December"};
24+
const char *const kEnShortDays[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
25+
const char *const kEnLongDays[7] = {"Sunday", "Monday", "Tuesday", "Wednesday",
26+
"Thursday", "Friday", "Saturday"};
27+
28+
// ---------------------------------------------------------------------------
29+
// Shared CJK building blocks (UTF-8 byte escapes).
30+
// ---------------------------------------------------------------------------
31+
#define VX_YUE "\xE6\x9C\x88" // U+6708 month
32+
#define VX_ZHOU "\xE5\x91\xA8" // U+5468 week (zh abbreviated day prefix)
33+
#define VX_XING "\xE6\x98\x9F" // U+661F
34+
#define VX_QI "\xE6\x9C\x9F" // U+671F
35+
#define VX_YAO "\xE6\x9B\x9C" // U+66DC (ja day-of-week marker)
36+
37+
#define VX_RI "\xE6\x97\xA5" // U+65E5 sun/day
38+
#define VX_YUE_MOON "\xE6\x9C\x88" // U+6708 moon (same glyph as month)
39+
#define VX_HUO "\xE7\x81\xAB" // U+706B fire
40+
#define VX_SHUI "\xE6\xB0\xB4" // U+6C34 water
41+
#define VX_MU "\xE6\x9C\xA8" // U+6728 wood
42+
#define VX_JIN "\xE9\x87\x91" // U+91D1 metal
43+
#define VX_TU "\xE5\x9C\x9F" // U+571F earth
44+
45+
#define VX_YI "\xE4\xB8\x80" // U+4E00 one
46+
#define VX_ER "\xE4\xBA\x8C" // U+4E8C two
47+
#define VX_SAN "\xE4\xB8\x89" // U+4E09 three
48+
#define VX_SI "\xE5\x9B\x9B" // U+56DB four
49+
#define VX_WU "\xE4\xBA\x94" // U+4E94 five
50+
#define VX_LIU "\xE5\x85\xAD" // U+516D six
51+
#define VX_QId "\xE4\xB8\x83" // U+4E03 seven
52+
#define VX_BA "\xE5\x85\xAB" // U+516B eight
53+
#define VX_JIU "\xE4\xB9\x9D" // U+4E5D nine
54+
#define VX_SHI "\xE5\x8D\x81" // U+5341 ten
55+
56+
// ---------------------------------------------------------------------------
57+
// Simplified Chinese (zh_CN)
58+
// ---------------------------------------------------------------------------
59+
const char *const kZhShortMonths[12] = {
60+
"1" VX_YUE, "2" VX_YUE, "3" VX_YUE, "4" VX_YUE, "5" VX_YUE, "6" VX_YUE,
61+
"7" VX_YUE, "8" VX_YUE, "9" VX_YUE, "10" VX_YUE, "11" VX_YUE, "12" VX_YUE};
62+
const char *const kZhLongMonths[12] = {
63+
VX_YI VX_YUE, VX_ER VX_YUE, VX_SAN VX_YUE, VX_SI VX_YUE,
64+
VX_WU VX_YUE, VX_LIU VX_YUE, VX_QId VX_YUE, VX_BA VX_YUE,
65+
VX_JIU VX_YUE, VX_SHI VX_YUE, VX_SHI VX_YI VX_YUE, VX_SHI VX_ER VX_YUE};
66+
const char *const kZhShortDays[7] = {VX_ZHOU VX_RI, VX_ZHOU VX_YI, VX_ZHOU VX_ER,
67+
VX_ZHOU VX_SAN, VX_ZHOU VX_SI, VX_ZHOU VX_WU,
68+
VX_ZHOU VX_LIU};
69+
const char *const kZhLongDays[7] = {
70+
VX_XING VX_QI VX_RI, VX_XING VX_QI VX_YI, VX_XING VX_QI VX_ER, VX_XING VX_QI VX_SAN,
71+
VX_XING VX_QI VX_SI, VX_XING VX_QI VX_WU, VX_XING VX_QI VX_LIU};
72+
73+
// ---------------------------------------------------------------------------
74+
// Japanese (ja)
75+
// ---------------------------------------------------------------------------
76+
const char *const kJaShortMonths[12] = {
77+
"1" VX_YUE, "2" VX_YUE, "3" VX_YUE, "4" VX_YUE, "5" VX_YUE, "6" VX_YUE,
78+
"7" VX_YUE, "8" VX_YUE, "9" VX_YUE, "10" VX_YUE, "11" VX_YUE, "12" VX_YUE};
79+
const char *const *const kJaLongMonths = kJaShortMonths;
80+
const char *const kJaShortDays[7] = {VX_RI, VX_YUE_MOON, VX_HUO, VX_SHUI,
81+
VX_MU, VX_JIN, VX_TU};
82+
const char *const kJaLongDays[7] = {
83+
VX_RI VX_YAO VX_RI, VX_YUE_MOON VX_YAO VX_RI, VX_HUO VX_YAO VX_RI, VX_SHUI VX_YAO VX_RI,
84+
VX_MU VX_YAO VX_RI, VX_JIN VX_YAO VX_RI, VX_TU VX_YAO VX_RI};
85+
86+
std::string NormalizeLocale(const std::string &locale) {
87+
std::string s;
88+
s.reserve(locale.size());
89+
for (char c : locale) {
90+
if (c == '-') {
91+
s.push_back('_');
92+
} else {
93+
s.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
94+
}
95+
}
96+
return s;
97+
}
98+
99+
} // namespace
100+
101+
std::string CanonicalizeLocale(const std::string &locale) {
102+
const std::string s = NormalizeLocale(locale);
103+
if (s.empty()) {
104+
return "en";
105+
}
106+
107+
// Exact match on the canonical names first.
108+
if (s == "en") return "en";
109+
if (s == "zh_cn") return "zh_CN";
110+
if (s == "ja") return "ja";
111+
112+
// Language subtag match.
113+
const std::string lang = s.substr(0, s.find('_'));
114+
if (lang == "zh") return "zh_CN";
115+
if (lang == "ja") return "ja";
116+
if (lang == "en") return "en";
117+
118+
return "en";
119+
}
120+
121+
const DateTimeNames &DateTimeNames::ForLocale(const std::string &locale) {
122+
static const DateTimeNames kEn("en", kEnShortMonths, kEnLongMonths, kEnShortDays, kEnLongDays);
123+
static const DateTimeNames kZh("zh_CN", kZhShortMonths, kZhLongMonths, kZhShortDays,
124+
kZhLongDays);
125+
static const DateTimeNames kJa("ja", kJaShortMonths, kJaLongMonths, kJaShortDays, kJaLongDays);
126+
127+
const std::string canonical = CanonicalizeLocale(locale);
128+
if (canonical == "zh_CN") return kZh;
129+
if (canonical == "ja") return kJa;
130+
return kEn;
131+
}
132+
133+
const char *DateTimeNames::ShortMonth(int tm_mon) const {
134+
if (tm_mon < 0 || tm_mon > 11) tm_mon = 0;
135+
return short_months_[tm_mon];
136+
}
137+
138+
const char *DateTimeNames::LongMonth(int tm_mon) const {
139+
if (tm_mon < 0 || tm_mon > 11) tm_mon = 0;
140+
return long_months_[tm_mon];
141+
}
142+
143+
const char *DateTimeNames::ShortDay(int tm_wday) const {
144+
if (tm_wday < 0 || tm_wday > 6) tm_wday = 0;
145+
return short_days_[tm_wday];
146+
}
147+
148+
const char *DateTimeNames::LongDay(int tm_wday) const {
149+
if (tm_wday < 0 || tm_wday > 6) tm_wday = 0;
150+
return long_days_[tm_wday];
151+
}
152+
153+
const char *DateTimeNames::CanonicalName() const { return canonical_name_; }
154+
155+
} // namespace vxcore

src/core/datetime_names.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#ifndef VXCORE_DATETIME_NAMES_H
2+
#define VXCORE_DATETIME_NAMES_H
3+
4+
#include <string>
5+
6+
namespace vxcore {
7+
8+
// UTF-8 month/day names for a supported locale. Immutable, statically allocated.
9+
//
10+
// The tables are locale-independent data compiled into the library: they never
11+
// consult the C runtime locale (`setlocale`/`LC_TIME`) or `strftime`, so the
12+
// bytes returned are always valid UTF-8 regardless of the host locale or the
13+
// Windows ANSI code page.
14+
class DateTimeNames {
15+
public:
16+
// |locale| accepts "en", "en_US", "zh_CN", "zh-CN", "ja_JP" (case-insensitive,
17+
// '-' normalized to '_'). Exact match, then language subtag, else English.
18+
// Never returns null. The returned object is statically allocated and
19+
// immutable; there is no global mutable state, so this is safe to call from
20+
// any thread.
21+
static const DateTimeNames &ForLocale(const std::string &locale);
22+
23+
const char *ShortMonth(int tm_mon) const; // 0..11, out of range -> January
24+
const char *LongMonth(int tm_mon) const;
25+
const char *ShortDay(int tm_wday) const; // 0..6, 0 = Sunday, out of range -> Sunday
26+
const char *LongDay(int tm_wday) const;
27+
const char *CanonicalName() const; // "en" | "zh_CN" | "ja"
28+
29+
private:
30+
DateTimeNames(const char *canonical_name, const char *const *short_months,
31+
const char *const *long_months, const char *const *short_days,
32+
const char *const *long_days)
33+
: canonical_name_(canonical_name),
34+
short_months_(short_months),
35+
long_months_(long_months),
36+
short_days_(short_days),
37+
long_days_(long_days) {}
38+
39+
const char *canonical_name_ = "en";
40+
const char *const *short_months_ = nullptr;
41+
const char *const *long_months_ = nullptr;
42+
const char *const *short_days_ = nullptr;
43+
const char *const *long_days_ = nullptr;
44+
};
45+
46+
// Canonical name a locale string resolves to; "" and unknown -> "en".
47+
std::string CanonicalizeLocale(const std::string &locale);
48+
49+
} // namespace vxcore
50+
51+
#endif // VXCORE_DATETIME_NAMES_H

0 commit comments

Comments
 (0)