@deepseek-ai/dsh-client-locale 0.1.6-alpha.1 → 0.1.7-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/locale/README.md
5
- README.md: 56a9cff9c3ec18dcc395378fcd1691207c022284
6
- README.zh.md: 1b7a7b7de48843961f3cbac3f4ee99cff641d212
5
+ README.md: a21f61dd931204f990e6865419ac19476c957ff4
6
+ README.zh.md: c93b555f96e459ddad56faaf46d267cf319218ad
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; loopback pages persist them in `$DSH_HOME/settings.yaml`, while non-loopback pages keep them only for the current process. New browsers use the first supported language requested by the browser until an allowed stored preference arrives. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload.
12
+ Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; loopback pages persist them in `$DSH_HOME/cordis.patch.yml`, while non-loopback pages keep them only for the current process. New browsers use the first supported language requested by the browser until an allowed stored preference arrives. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -31,10 +31,16 @@ Use it wherever the web GUI needs a language switch or translated copy: the ship
31
31
 
32
32
  Open Settings → General and select a registered language. The active locale is applied immediately: the UI copy switches, `<html lang>` points at the external id or built-in document tag, and the choice is written to the durable settings section. A browser without an explicit Host preference selects the first registered language that matches `navigator` by full tag and then primary subtag, falling back to English. A stored external locale waits for its definition to register instead of becoming active while unavailable.
33
33
 
34
+ Native shells may provide `__DSH_LOCALE__` with an asynchronous `read()` and an `onChange(locale)` callback. Initialization supplies the current Host preference and ordered OS languages before the Client mounts. Automatic selection stays provisional; only Settings selections write `locale.preference`. A fresh read on each page load prevents a stale preload preference after reload. Ordinary browsers keep navigator-based detection and their existing settings-scope policy.
35
+
34
36
  ### Registering a dictionary
35
37
 
36
38
  Call `ctx.locale.register(ns, { zh, en })` with a namespace merged into `LocaleNamespaceMap`; the compiler checks every key against the namespace's typed key union and requires both shipped locales. Consumers translate through `ctx.locale.bind(ns)` or the framework-injected `t` seat. A dictionary registered after the UI is already mounted is picked up without a remount.
37
39
 
40
+ ### Resolving package text
41
+
42
+ Use `ctx.locale.resolveText(text)` for [`LocalizedText`](../../util/package-manifest/README.md), such as installed plugin titles and descriptions. Literal strings are returned unchanged. Translation maps use lowercase language ids, require an `en` fallback, and follow the active language's declared fallback chain. They do not consult or register namespace dictionaries.
43
+
38
44
  ### Registering a language pack
39
45
 
40
46
  An external client plugin registers the language definition and each translated namespace as owned effects; definitions and dictionaries may register in either order:
@@ -83,6 +89,8 @@ The provisional locale comes from the browser (`navigator.languages` matched by
83
89
 
84
90
  ### Dictionary lookup
85
91
 
92
+ Document language synchronization writes `<html lang>` only when its value changes; dictionary-only revisions leave the attribute untouched.
93
+
86
94
  The typed object form requires complete dictionaries for both built-in locales. The per-locale form lets language packs register each namespace independently. For each key, lookup walks the active language's declared fallback chain in the requested namespace, repeats that chain in `common`, then displays the key itself. Bound translate functions retain stable identity per namespace so they can ride inject surfaces without breaking memoization.
87
95
 
88
96
  ### Source map
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文和中文 locale,或 client 插件添加的语言。用户选择会立即生效;loopback 页面把选择持久化到 `$DSH_HOME/settings.yaml`,非 loopback 页面则只为当前进程保留选择。全新浏览器会使用浏览器请求的第一个受支持语言,直到允许读取的已存储偏好到达。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。
12
+ 使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文和中文 locale,或 client 插件添加的语言。用户选择会立即生效;loopback 页面把选择持久化到 `$DSH_HOME/cordis.patch.yml`,非 loopback 页面则只为当前进程保留选择。全新浏览器会使用浏览器请求的第一个受支持语言,直到允许读取的已存储偏好到达。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。
13
13
 
14
14
  ## 目录
15
15
 
@@ -31,10 +31,16 @@ kind: "package-reference"
31
31
 
32
32
  打开“设置 → 常规”并选择一种已注册语言。生效中的 locale 会立即应用:UI 文案切换、`<html lang>` 指向外部 id 或内置语言的文档标签,选择写入持久设置分区。没有显式 Host 偏好的浏览器会按完整标签、再按主语言子标签选择 `navigator` 请求的第一个已注册语言,无法匹配时回退到英文。已存储的外部 locale 会等待其定义注册,不会在不可用时生效。
33
33
 
34
+ 原生壳可以提供包含异步 `read()` 和 `onChange(locale)` 回调的 `__DSH_LOCALE__`。初始化在 Client 挂载前提供当前 Host 偏好和有序的系统语言列表。自动选择保持临时状态;只有设置中的选择会写入 `locale.preference`。每次加载页面都重新读取,避免重载后沿用过期的 preload 偏好。普通浏览器继续使用 navigator 检测和原有的设置作用域策略。
35
+
34
36
  ### 注册字典
35
37
 
36
38
  用已合并进 `LocaleNamespaceMap` 的命名空间调用 `ctx.locale.register(ns, { zh, en })`;编译器会对照该命名空间的类型化键并集检查每个键,并要求两个内置 locale 齐全。消费方通过 `ctx.locale.bind(ns)` 或框架注入的 `t` 席位翻译。UI 已挂载后再注册的字典无需重新挂载即可生效。
37
39
 
40
+ ### 解析包文本
41
+
42
+ 使用 `ctx.locale.resolveText(text)` 解析 [`LocalizedText`](../../util/package-manifest/README.zh.md),例如已安装插件的标题与描述。字面字符串原样返回。翻译映射使用小写语言 id,必须提供 `en` 回退值,并沿当前语言声明的回退链查找。它们不查询或注册命名空间字典。
43
+
38
44
  ### 注册语言包
39
45
 
40
46
  外部 client 插件把语言定义和每个已翻译命名空间注册为自身拥有的 effect;定义与字典可以按任意顺序注册:
@@ -83,6 +89,8 @@ Host 通过 settings 服务为 loopback 页面持久化偏好。Client 会刻意
83
89
 
84
90
  ### 字典查找
85
91
 
92
+ 文档语言同步只在值变化时写入 `<html lang>`;仅更新字典的 revision 不改动该属性。
93
+
86
94
  带类型的对象形式要求两个内置 locale 都有完整字典;逐 locale 形式允许语言包独立注册每个命名空间。逐键查找会先在请求命名空间中沿生效语言声明的 fallback 链查找,再在 `common` 中重复该链,最后显示键本身。绑定的翻译函数按命名空间保持稳定身份,因此可通过 inject 机制传递,且不会破坏 memoization。
87
95
 
88
96
  ### 源码地图
package/lib/client.js CHANGED
@@ -8,7 +8,22 @@ window.__ModuleLoader__.load({
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
10
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
- //#region ../../../vendor/cosmokit/src/misc.ts
11
+ //#region lib/types/client/bootstrap.js
12
+ /** Locale initialization supplied by a native shell before its Client tree mounts. */
13
+ /**
14
+ * Validate initialization data returned over the preload IPC bridge.
15
+ * @param value - untrusted IPC response.
16
+ * @returns language initialization with no persistence side effects.
17
+ */
18
+ function parseLocaleBootstrap(value) {
19
+ if (typeof value !== "object" || value === null || !("languages" in value) || !Array.isArray(value.languages) || !value.languages.every((language) => typeof language === "string") || !("preference" in value) || value.preference !== null && typeof value.preference !== "string") throw new TypeError("locale: invalid native initialization data");
20
+ return {
21
+ languages: value.languages,
22
+ preference: value.preference
23
+ };
24
+ }
25
+ //#endregion
26
+ //#region ../../../vendor/cosmokit/lib/index.js
12
27
  /** Return true when a value is `null` or `undefined`. */
13
28
  function isNullable(value) {
14
29
  return value === null || value === void 0;
@@ -32,8 +47,43 @@ window.__ModuleLoader__.load({
32
47
  for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
33
48
  return result;
34
49
  }
35
- //#endregion
36
- //#region ../../../vendor/cosmokit/src/types.ts
50
+ /** Shared config references used by schema validators and plugin runtimes. */
51
+ const write = Symbol.for("cosmokit.volatile.write");
52
+ function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
53
+ if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
54
+ if (value === null || typeof value !== "object") return value;
55
+ if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
56
+ ancestors.add(value);
57
+ try {
58
+ if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
59
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
60
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
61
+ } finally {
62
+ ancestors.delete(value);
63
+ }
64
+ }
65
+ /**
66
+ * Create a detached reference containing an immutable copy of the supplied data.
67
+ * @param value - validated config data; class instances and functions are unsupported.
68
+ * @returns a reference whose value is updated only by its owning runtime.
69
+ */
70
+ function createVolatile(value) {
71
+ let current = snapshot(value);
72
+ return Object.freeze({
73
+ get: () => current,
74
+ [write]: (value) => {
75
+ current = value;
76
+ }
77
+ });
78
+ }
79
+ /**
80
+ * Identify references across ESM/CJS copies of the shared library.
81
+ * @param value - a parsed config value.
82
+ * @returns whether the value implements the shared reference protocol.
83
+ */
84
+ function isVolatile(value) {
85
+ return typeof value === "object" && value !== null && write in value;
86
+ }
37
87
  /** Test values using `instanceof` with a `toStringTag` fallback. */
38
88
  function is(type, value) {
39
89
  if (arguments.length === 1) return (value) => is(type, value);
@@ -45,15 +95,16 @@ window.__ModuleLoader__.load({
45
95
  function isArrayBufferSource(value) {
46
96
  return isArrayBufferLike(value) || ArrayBuffer.isView(value);
47
97
  }
48
- let Binary;
49
- (function(_Binary) {
50
- _Binary.is = isArrayBufferLike;
51
- _Binary.isSource = isArrayBufferSource;
98
+ /** Binary source detection and base64/hex conversion helpers. */
99
+ var Binary;
100
+ (function(Binary) {
101
+ Binary.is = isArrayBufferLike;
102
+ Binary.isSource = isArrayBufferSource;
52
103
  function fromSource(source) {
53
104
  if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
54
105
  else return source;
55
106
  }
56
- _Binary.fromSource = fromSource;
107
+ Binary.fromSource = fromSource;
57
108
  function toBase64(source) {
58
109
  source = fromSource(source);
59
110
  if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
@@ -62,18 +113,18 @@ window.__ModuleLoader__.load({
62
113
  for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
63
114
  return btoa(binary);
64
115
  }
65
- _Binary.toBase64 = toBase64;
116
+ Binary.toBase64 = toBase64;
66
117
  function fromBase64(source) {
67
118
  if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
68
119
  return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
69
120
  }
70
- _Binary.fromBase64 = fromBase64;
121
+ Binary.fromBase64 = fromBase64;
71
122
  function toHex(source) {
72
123
  source = fromSource(source);
73
124
  if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
74
125
  return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
75
126
  }
76
- _Binary.toHex = toHex;
127
+ Binary.toHex = toHex;
77
128
  function fromHex(source) {
78
129
  if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
79
130
  const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
@@ -81,7 +132,7 @@ window.__ModuleLoader__.load({
81
132
  for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
82
133
  return Uint8Array.from(buffer).buffer;
83
134
  }
84
- _Binary.fromHex = fromHex;
135
+ Binary.fromHex = fromHex;
85
136
  })(Binary || (Binary = {}));
86
137
  Binary.fromBase64;
87
138
  Binary.toBase64;
@@ -113,58 +164,78 @@ window.__ModuleLoader__.load({
113
164
  }
114
165
  return result;
115
166
  }
116
- /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
167
+ /**
168
+ * Compare values recursively, treating two volatile references as equal regardless of value.
169
+ * Strict comparison distinguishes null/undefined, treats opaque objects by identity,
170
+ * compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
171
+ * @param a - first value.
172
+ * @param b - second value.
173
+ * @param strict - whether to require strict data equality outside volatile references.
174
+ * @returns whether the values compare equal.
175
+ */
117
176
  function deepEqual(a, b, strict) {
118
- if (a === b) return true;
119
- if (!strict && isNullable(a) && isNullable(b)) return true;
120
- if (typeof a !== typeof b) return false;
121
- if (typeof a !== "object") return false;
122
- if (!a || !b) return false;
123
- function check(test, then) {
124
- return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
177
+ const ancestors = /* @__PURE__ */ new Set();
178
+ function compare(a, b) {
179
+ if (a === b) return true;
180
+ if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
181
+ if (!strict && isNullable(a) && isNullable(b)) return true;
182
+ if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
183
+ if (ancestors.has(a)) return false;
184
+ function check(test, then) {
185
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
186
+ }
187
+ ancestors.add(a);
188
+ try {
189
+ return check(Array.isArray, (a, b) => {
190
+ if (a.length !== b.length) return false;
191
+ for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
192
+ return true;
193
+ }) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
194
+ if (a.byteLength !== b.byteLength) return false;
195
+ const viewA = new Uint8Array(a);
196
+ const viewB = new Uint8Array(b);
197
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
198
+ return true;
199
+ }) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
200
+ ...a,
201
+ ...b
202
+ }).every((key) => compare(a[key], b[key])));
203
+ } finally {
204
+ ancestors.delete(a);
205
+ }
125
206
  }
126
- return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
127
- if (a.byteLength !== b.byteLength) return false;
128
- const viewA = new Uint8Array(a);
129
- const viewB = new Uint8Array(b);
130
- for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
131
- return true;
132
- }) ?? Object.keys({
133
- ...a,
134
- ...b
135
- }).every((key) => deepEqual(a[key], b[key], strict));
207
+ return compare(a, b);
136
208
  }
137
- //#endregion
138
- //#region ../../../vendor/cosmokit/src/time.ts
139
- let Time;
140
- (function(_Time) {
141
- _Time.millisecond = 1;
142
- const second = _Time.second = 1e3;
143
- const minute = _Time.minute = second * 60;
144
- const hour = _Time.hour = minute * 60;
145
- const day = _Time.day = hour * 24;
146
- const week = _Time.week = day * 7;
209
+ /** Time constants plus parsing and formatting helpers. */
210
+ var Time;
211
+ (function(Time) {
212
+ Time.millisecond = 1;
213
+ Time.second = 1e3;
214
+ Time.minute = Time.second * 60;
215
+ Time.hour = Time.minute * 60;
216
+ Time.day = Time.hour * 24;
217
+ Time.week = Time.day * 7;
147
218
  let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
148
219
  function setTimezoneOffset(offset) {
149
220
  timezoneOffset = offset;
150
221
  }
151
- _Time.setTimezoneOffset = setTimezoneOffset;
222
+ Time.setTimezoneOffset = setTimezoneOffset;
152
223
  function getTimezoneOffset() {
153
224
  return timezoneOffset;
154
225
  }
155
- _Time.getTimezoneOffset = getTimezoneOffset;
226
+ Time.getTimezoneOffset = getTimezoneOffset;
156
227
  function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
157
228
  if (typeof date === "number") date = new Date(date);
158
229
  if (offset === void 0) offset = timezoneOffset;
159
- return Math.floor((date.valueOf() / minute - offset) / 1440);
230
+ return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
160
231
  }
161
- _Time.getDateNumber = getDateNumber;
232
+ Time.getDateNumber = getDateNumber;
162
233
  function fromDateNumber(value, offset) {
163
- const date = new Date(value * day);
234
+ const date = new Date(value * Time.day);
164
235
  if (offset === void 0) offset = timezoneOffset;
165
- return new Date(+date + offset * minute);
236
+ return new Date(+date + offset * Time.minute);
166
237
  }
167
- _Time.fromDateNumber = fromDateNumber;
238
+ Time.fromDateNumber = fromDateNumber;
168
239
  const numeric = /\d+(?:\.\d+)?/.source;
169
240
  const timeRegExp = new RegExp(`^${[
170
241
  "w(?:eek(?:s)?)?",
@@ -176,9 +247,9 @@ window.__ModuleLoader__.load({
176
247
  function parseTime(source) {
177
248
  const capture = timeRegExp.exec(source);
178
249
  if (!capture) return 0;
179
- return (parseFloat(capture[1]) * week || 0) + (parseFloat(capture[2]) * day || 0) + (parseFloat(capture[3]) * hour || 0) + (parseFloat(capture[4]) * minute || 0) + (parseFloat(capture[5]) * second || 0);
250
+ return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
180
251
  }
181
- _Time.parseTime = parseTime;
252
+ Time.parseTime = parseTime;
182
253
  function parseDate(date) {
183
254
  const parsed = parseTime(date);
184
255
  if (parsed) date = Date.now() + parsed;
@@ -186,27 +257,27 @@ window.__ModuleLoader__.load({
186
257
  else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
187
258
  return date ? new Date(date) : /* @__PURE__ */ new Date();
188
259
  }
189
- _Time.parseDate = parseDate;
260
+ Time.parseDate = parseDate;
190
261
  function format(ms) {
191
262
  const abs = Math.abs(ms);
192
- if (abs >= day - hour / 2) return Math.round(ms / day) + "d";
193
- else if (abs >= hour - minute / 2) return Math.round(ms / hour) + "h";
194
- else if (abs >= minute - second / 2) return Math.round(ms / minute) + "m";
195
- else if (abs >= second) return Math.round(ms / second) + "s";
263
+ if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
264
+ else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
265
+ else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
266
+ else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
196
267
  return ms + "ms";
197
268
  }
198
- _Time.format = format;
269
+ Time.format = format;
199
270
  function toDigits(source, length = 2) {
200
271
  return source.toString().padStart(length, "0");
201
272
  }
202
- _Time.toDigits = toDigits;
273
+ Time.toDigits = toDigits;
203
274
  function template(template, time = /* @__PURE__ */ new Date()) {
204
275
  return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
205
276
  }
206
- _Time.template = template;
277
+ Time.template = template;
207
278
  })(Time || (Time = {}));
208
279
  //#endregion
209
- //#region ../../../vendor/schemastery/src/index.ts
280
+ //#region ../../../vendor/schemastery/lib/index.mjs
210
281
  const kSchema = Symbol.for("schemastery");
211
282
  const kValidationError = Symbol.for("ValidationError");
212
283
  globalThis.__schemastery_index__ ??= 0;
@@ -382,6 +453,7 @@ window.__ModuleLoader__.load({
382
453
  return schema;
383
454
  };
384
455
  Schema.prototype.simplify = function simplify(value) {
456
+ if (isVolatile(value)) value = value.get();
385
457
  if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
386
458
  if (isNullable(value)) return value;
387
459
  if (this.type === "object" || this.type === "dict") {
@@ -438,12 +510,49 @@ window.__ModuleLoader__.load({
438
510
  };
439
511
  return schema;
440
512
  } });
513
+ Schema.prototype.volatile = function volatile() {
514
+ if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
515
+ return this.extra("volatile", true);
516
+ };
441
517
  const resolvers = {};
518
+ const checkedVolatile = Symbol("checked-volatile-schema");
519
+ function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
520
+ const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
521
+ if (states.has(blocked)) return;
522
+ states.add(blocked);
523
+ seen.set(schema, states);
524
+ if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
525
+ const nested = blocked || !!schema.meta?.volatile;
526
+ if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
527
+ if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
528
+ if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
529
+ if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
530
+ }
442
531
  Schema.extend = function extend(type, resolve) {
443
532
  resolvers[type] = resolve;
444
533
  };
445
534
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
446
535
  if (!schema) return [data];
536
+ if (!options[checkedVolatile]) {
537
+ validateVolatileSchema(schema, options.path);
538
+ options = {
539
+ ...options,
540
+ [checkedVolatile]: true
541
+ };
542
+ }
543
+ if (schema.meta?.volatile) {
544
+ const inner = Schema(schema);
545
+ inner.meta = {
546
+ ...schema.meta,
547
+ volatile: false
548
+ };
549
+ const [value, adapted] = Schema.resolve(data, inner, options, strict);
550
+ try {
551
+ return [createVolatile(value), adapted];
552
+ } catch (error) {
553
+ throw new ValidationError(error instanceof Error ? error.message : String(error), options);
554
+ }
555
+ }
447
556
  if (options.ignore?.(data, schema)) return [data];
448
557
  if (isNullable(data) && schema.type !== "lazy") {
449
558
  if (schema.meta.required) throw new ValidationError(`missing required value`, options);
@@ -546,6 +655,7 @@ window.__ModuleLoader__.load({
546
655
  ...schema.meta,
547
656
  ...schema.inner.meta
548
657
  };
658
+ validateVolatileSchema(schema.inner, options.path, true);
549
659
  }
550
660
  return Schema.resolve(data, schema.inner, options, strict);
551
661
  });
@@ -645,7 +755,7 @@ window.__ModuleLoader__.load({
645
755
  } catch (e) {
646
756
  if (!options?.autofix) throw e;
647
757
  delete data[key];
648
- return schema.meta.default;
758
+ return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
649
759
  }
650
760
  }
651
761
  Schema.extend("array", (data, { inner, meta }, options) => {
@@ -810,7 +920,9 @@ window.__ModuleLoader__.load({
810
920
  const LOCALE_ID_PATTERN = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
811
921
  /** Locale identifiers shipped by the browser client. */
812
922
  const LOCALE_IDS = ["zh", "en"];
813
- Schema.object({ [LOCALE_PREFERENCE_FIELD]: Schema.string().pattern(LOCALE_ID_PATTERN).required(false) });
923
+ /** Durable locale schema; also the wire envelope the browser scope validates against. */
924
+ const LocaleSettingsFields = { [LOCALE_PREFERENCE_FIELD]: Schema.string().pattern(LOCALE_ID_PATTERN).required(false) };
925
+ Schema.object(LocaleSettingsFields);
814
926
  //#endregion
815
927
  //#region lib/types/locales/zh.js
816
928
  /** zh base dictionary for the common namespace: cross-feature standard words. */
@@ -970,7 +1082,7 @@ window.__ModuleLoader__.load({
970
1082
  onClick: () => {
971
1083
  setOpen((v) => !v);
972
1084
  },
973
- children: [activeLabel, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: LanguageRow_module_css_default.chevron })]
1085
+ children: [activeLabel, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: LanguageRow_module_css_default.chevron })]
974
1086
  })
975
1087
  })]
976
1088
  });
@@ -1051,7 +1163,8 @@ window.__ModuleLoader__.load({
1051
1163
  */
1052
1164
  function syncDocumentLanguage(snapshot) {
1053
1165
  if (typeof document === "undefined") return;
1054
- document.documentElement.lang = snapshot.active === "zh" ? "zh-CN" : snapshot.active;
1166
+ const language = snapshot.active === "zh" ? "zh-CN" : snapshot.active;
1167
+ if (document.documentElement.lang !== language) document.documentElement.lang = language;
1055
1168
  }
1056
1169
  /**
1057
1170
  * Dictionary registry plus locale preference. Lookup walks the active
@@ -1064,6 +1177,7 @@ window.__ModuleLoader__.load({
1064
1177
  * `ctx.slots.installLocale`.
1065
1178
  */
1066
1179
  var LocaleRuntime = class {
1180
+ bootstrap;
1067
1181
  dicts = /* @__PURE__ */ new Map();
1068
1182
  bound = /* @__PURE__ */ new Map();
1069
1183
  catalog = /* @__PURE__ */ new Map();
@@ -1081,15 +1195,18 @@ window.__ModuleLoader__.load({
1081
1195
  * listener is released through ctx.effect on dispose).
1082
1196
  * @param host - durable preference scope owned by the providing plugin;
1083
1197
  * absent compositions (standalone dictionary registries) stay process-local.
1198
+ * @param bootstrap - native initialization; absent in ordinary browsers.
1084
1199
  */
1085
- constructor(ctx, host) {
1200
+ constructor(ctx, host, bootstrap) {
1201
+ this.bootstrap = bootstrap;
1086
1202
  this.ctx = ctx;
1087
1203
  this.host = host;
1088
1204
  for (const locale of BUILT_IN_LOCALES) this.catalog.set(localeKey(locale.id), locale);
1089
1205
  const locales = this.localeList();
1090
- this.provisional = resolveInitialLocale(locales);
1206
+ this.provisional = resolveInitialLocale(locales, bootstrap?.languages);
1207
+ this.preference = bootstrap?.preference ?? void 0;
1091
1208
  this.snapshot = Object.freeze({
1092
- active: this.provisional,
1209
+ active: this.resolveActive(),
1093
1210
  locales,
1094
1211
  revision: 0
1095
1212
  });
@@ -1108,6 +1225,16 @@ window.__ModuleLoader__.load({
1108
1225
  return this.snapshot;
1109
1226
  }
1110
1227
  /**
1228
+ * Resolve package text through the active language's declared fallback chain.
1229
+ * Plain strings stay verbatim; maps do not consult registered dictionaries.
1230
+ * @param text - package text whose locale keys are lowercase and include English.
1231
+ * @returns the first available translation, including an empty string.
1232
+ */
1233
+ resolveText(text) {
1234
+ if (typeof text === "string") return text;
1235
+ return this.fallbackChain(this.snapshot.active).reduceRight((resolved, locale) => text[localeKey(locale)] ?? resolved, text.en);
1236
+ }
1237
+ /**
1111
1238
  * LocaleFace getSnapshot: the current snapshot (carries `revision`; stable
1112
1239
  * reference between changes, uSES-safe).
1113
1240
  * @returns the current snapshot.
@@ -1200,7 +1327,7 @@ window.__ModuleLoader__.load({
1200
1327
  publishCatalog() {
1201
1328
  this.fallbackChains.clear();
1202
1329
  const locales = this.localeList();
1203
- this.provisional = resolveInitialLocale(locales);
1330
+ this.provisional = resolveInitialLocale(locales, this.bootstrap?.languages);
1204
1331
  const active = this.resolveActive();
1205
1332
  this.publish(active, active !== this.snapshot.active, locales);
1206
1333
  }
@@ -1323,8 +1450,8 @@ window.__ModuleLoader__.load({
1323
1450
  * The browser's own language wins over {@link FALLBACK_LOCALE}; an explicit
1324
1451
  * Host preference may replace this provisional value after plugin activation.
1325
1452
  */
1326
- function resolveInitialLocale(locales) {
1327
- return detectBrowserLocale(locales) ?? "en";
1453
+ function resolveInitialLocale(locales, languages) {
1454
+ return detectBrowserLocale(locales, languages) ?? "en";
1328
1455
  }
1329
1456
  /**
1330
1457
  * The first registered locale the browser asks for. Each browser tag first
@@ -1335,12 +1462,15 @@ window.__ModuleLoader__.load({
1335
1462
  * locale for non-browser runs. `navigator.language` trails the ordered
1336
1463
  * `languages` list and covers hosts exposing only the single tag.
1337
1464
  * @param locales - definitions currently available to the browser.
1465
+ * @param languages - native system language order, when supplied by a shell.
1338
1466
  * @returns the first matching locale id, or undefined.
1339
1467
  */
1340
- function detectBrowserLocale(locales) {
1341
- if (typeof window === "undefined") return void 0;
1342
- const languages = navigator.languages;
1343
- for (const tag of [...languages ?? [], navigator.language]) {
1468
+ function detectBrowserLocale(locales, languages) {
1469
+ if (languages === void 0) {
1470
+ if (typeof window === "undefined") return void 0;
1471
+ languages = [...navigator.languages ?? [], navigator.language];
1472
+ }
1473
+ for (const tag of languages) {
1344
1474
  const requested = localeKey(tag);
1345
1475
  const exact = locales.find((locale) => localeKey(locale.id) === requested);
1346
1476
  if (exact !== void 0) return exact.id;
@@ -1353,16 +1483,30 @@ window.__ModuleLoader__.load({
1353
1483
  const inject = [
1354
1484
  "slots",
1355
1485
  "remote",
1356
- "settingsScope"
1486
+ "configForms"
1357
1487
  ];
1358
1488
  /**
1359
1489
  * Client plugin body: provide the locale service with base dictionaries and
1360
1490
  * register the feature-owned Language preference row into the General
1361
1491
  * section's item slot (a feature owns its settings surface).
1362
1492
  * @param ctx - client cordis context.
1493
+ * @returns resolves after native language initialization and plugin registration.
1363
1494
  */
1364
- function apply(ctx) {
1365
- const locale = new LocaleRuntime(ctx, ctx.settingsScope.bind({ namespace: LOCALE_SETTINGS_NAMESPACE }));
1495
+ async function apply(ctx) {
1496
+ const bridge = globalThis.__DSH_LOCALE__;
1497
+ let bootstrap;
1498
+ if (bridge !== void 0) {
1499
+ let value;
1500
+ try {
1501
+ value = await bridge.read();
1502
+ } catch (error) {
1503
+ if (ctx.fiber.uid === null) return;
1504
+ throw error;
1505
+ }
1506
+ if (ctx.fiber.uid === null) return;
1507
+ bootstrap = parseLocaleBootstrap(value);
1508
+ }
1509
+ const locale = new LocaleRuntime(ctx, ctx.configForms.get(LOCALE_SETTINGS_NAMESPACE), bootstrap);
1366
1510
  locale.register(COMMON_NS, {
1367
1511
  zh: zh$1,
1368
1512
  en: en$1
@@ -1372,6 +1516,12 @@ window.__ModuleLoader__.load({
1372
1516
  en
1373
1517
  });
1374
1518
  ctx.provide("locale", locale);
1519
+ if (bridge !== void 0) {
1520
+ ctx.on("locale/change", (snapshot) => {
1521
+ bridge.onChange(snapshot.active);
1522
+ });
1523
+ bridge.onChange(locale.getSnapshot().active);
1524
+ }
1375
1525
  ctx.slots.installLocale(locale);
1376
1526
  const store = createLanguageRowStore();
1377
1527
  let bound;
package/lib/index.js CHANGED
@@ -10,18 +10,19 @@ const LOCALE_ID_PATTERN = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
10
10
  /** Locale identifiers shipped by the browser client. */
11
11
  const LOCALE_IDS = ["zh", "en"];
12
12
  /** Durable locale schema; also the wire envelope the browser scope validates against. */
13
- const LocaleSettingsSchema = z.object({ [LOCALE_PREFERENCE_FIELD]: z.string().pattern(LOCALE_ID_PATTERN).required(false) });
13
+ const LocaleSettingsFields = { [LOCALE_PREFERENCE_FIELD]: z.string().pattern(LOCALE_ID_PATTERN).required(false) };
14
+ z.object(LocaleSettingsFields);
14
15
  //#endregion
15
16
  //#region lib/types/index.js
16
- /** Host registration for the browser locale preference. */
17
- /**
18
- * Register the durable locale section when a settings provider exists.
19
- * @param ctx - Host context whose optional settings service owns the section.
17
+ /** Live preferences projected to the browser. */
18
+ const Config = z.object({ [LOCALE_PREFERENCE_FIELD]: LocaleSettingsFields[LOCALE_PREFERENCE_FIELD].volatile() });
19
+ /** Host preferences are consumed through the configuration form projection.
20
+ * @param ctx Plugin context used for optional settings presentation.
20
21
  */
21
22
  function apply(ctx) {
22
- ctx.inject(["settings"], (settingsCtx) => {
23
- settingsCtx.settings.register(LOCALE_SETTINGS_NAMESPACE, LocaleSettingsSchema);
23
+ ctx.inject(["settings"], (child) => {
24
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
24
25
  });
25
26
  }
26
27
  //#endregion
27
- export { LOCALE_IDS, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, apply };
28
+ export { Config, LOCALE_IDS, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, apply };
@@ -0,0 +1,22 @@
1
+ /** Locale initialization supplied by a native shell before its Client tree mounts. */
2
+ /** System languages and the existing Host preference for one page load. */
3
+ export interface LocaleBootstrap {
4
+ /** Operating-system language tags in preference order. */
5
+ readonly languages: readonly string[];
6
+ /** Stored locale.preference; null preserves automatic language selection. */
7
+ readonly preference: string | null;
8
+ }
9
+ /** Optional page-global bridge named __DSH_LOCALE__; ordinary browsers omit it. */
10
+ export interface LocaleBridge {
11
+ /** Read current initialization data through the shell's isolated preload. @returns unvalidated IPC data. */
12
+ read(): Promise<unknown>;
13
+ /** Publish the resolved Client locale without writing another preference. @param locale - active registered locale id. */
14
+ onChange(locale: string): void;
15
+ }
16
+ /**
17
+ * Validate initialization data returned over the preload IPC bridge.
18
+ * @param value - untrusted IPC response.
19
+ * @returns language initialization with no persistence side effects.
20
+ */
21
+ export declare function parseLocaleBootstrap(value: unknown): LocaleBootstrap;
22
+ //# sourceMappingURL=bootstrap.d.ts.map
@@ -5,8 +5,10 @@
5
5
  * its own settings surface.
6
6
  */
7
7
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
8
+ import type { LocalizedText } from '@deepseek-ai/dsh-package-manifest';
8
9
  import { type LocaleDictOf, type LocaleNamespaceMap, type Translate, type TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
9
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
10
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
11
+ import { type LocaleBootstrap } from './bootstrap.ts';
10
12
  import { type BuiltInLocaleId, type LocaleId, type LocaleSettings } from '../locale-settings.ts';
11
13
  import { type CommonKey } from '../locales/index.ts';
12
14
  import { type SettingsLocaleKey } from '../locales/settings.ts';
@@ -93,6 +95,7 @@ export declare const SETTINGS_NS = "settings.locale";
93
95
  * `ctx.slots.installLocale`.
94
96
  */
95
97
  export declare class LocaleRuntime {
98
+ private readonly bootstrap?;
96
99
  private dicts;
97
100
  private bound;
98
101
  private catalog;
@@ -110,13 +113,21 @@ export declare class LocaleRuntime {
110
113
  * listener is released through ctx.effect on dispose).
111
114
  * @param host - durable preference scope owned by the providing plugin;
112
115
  * absent compositions (standalone dictionary registries) stay process-local.
116
+ * @param bootstrap - native initialization; absent in ordinary browsers.
113
117
  */
114
- constructor(ctx: ClientContext, host?: SettingsScope<LocaleSettings>);
118
+ constructor(ctx: ClientContext, host?: ConfigForm<LocaleSettings>, bootstrap?: LocaleBootstrap | undefined);
115
119
  /**
116
120
  * Read the current immutable locale snapshot.
117
121
  * @returns the current snapshot (stable reference until the next change).
118
122
  */
119
123
  getLocale(): LocaleSnapshot;
124
+ /**
125
+ * Resolve package text through the active language's declared fallback chain.
126
+ * Plain strings stay verbatim; maps do not consult registered dictionaries.
127
+ * @param text - package text whose locale keys are lowercase and include English.
128
+ * @returns the first available translation, including an empty string.
129
+ */
130
+ resolveText(text: LocalizedText): string;
120
131
  /**
121
132
  * LocaleFace getSnapshot: the current snapshot (carries `revision`; stable
122
133
  * reference between changes, uSES-safe).
@@ -231,6 +242,7 @@ export declare const inject: string[];
231
242
  * register the feature-owned Language preference row into the General
232
243
  * section's item slot (a feature owns its settings surface).
233
244
  * @param ctx - client cordis context.
245
+ * @returns resolves after native language initialization and plugin registration.
234
246
  */
235
- export declare function apply(ctx: ClientContext): void;
247
+ export declare function apply(ctx: ClientContext): Promise<void>;
236
248
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,19 @@
1
- /** Host registration for the browser locale preference. */
2
- import type { Context } from '@deepseek-ai/cordis';
1
+ import type { Volatile, Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
3
  export { LOCALE_IDS, LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, type BuiltInLocaleId, type LocaleId, type LocaleSettings, } from './locale-settings.ts';
4
- /**
5
- * Register the durable locale section when a settings provider exists.
6
- * @param ctx - Host context whose optional settings service owns the section.
4
+ /** Runtime preferences projected to the browser. */
5
+ export interface Config {
6
+ /** Explicit locale; omission follows the browser. */
7
+ preference: Volatile<string | undefined>;
8
+ }
9
+ /** Live preferences projected to the browser. */
10
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
11
+ preference: z<string, string, "volatile">;
12
+ }>>, Schemastery.ObjectT<NoInfer<{
13
+ preference: z<string, string, "volatile">;
14
+ }>>, "plain">;
15
+ /** Host preferences are consumed through the configuration form projection.
16
+ * @param ctx Plugin context used for optional settings presentation.
7
17
  */
8
18
  export declare function apply(ctx: Context): void;
9
19
  //# sourceMappingURL=index.d.ts.map
@@ -18,5 +18,13 @@ export interface LocaleSettings {
18
18
  preference?: LocaleId;
19
19
  }
20
20
  /** Durable locale schema; also the wire envelope the browser scope validates against. */
21
- export declare const LocaleSettingsSchema: z<LocaleSettings>;
21
+ export declare const LocaleSettingsFields: {
22
+ preference: z<string, string, "plain">;
23
+ };
24
+ /** Schema for the shared locale preference. */
25
+ export declare const LocaleSettingsSchema: z<Schemastery.ObjectS<NoInfer<{
26
+ preference: z<string, string, "plain">;
27
+ }>>, Schemastery.ObjectT<NoInfer<{
28
+ preference: z<string, string, "plain">;
29
+ }>>, "plain">;
22
30
  //# sourceMappingURL=locale-settings.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-locale",
3
3
  "description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries",
4
- "version": "0.1.6-alpha.1",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -39,24 +39,25 @@
39
39
  },
40
40
  "license": "MIT",
41
41
  "peerDependencies": {
42
- "@deepseek-ai/cordis": "^4.0.2"
42
+ "@deepseek-ai/cordis": "^4.0.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/react": "~18.3.1",
46
46
  "react": "^18.2.0",
47
- "@deepseek-ai/cordis": "^4.0.2",
48
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.6-alpha.1",
49
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
50
- "@deepseek-ai/dsh-api-remotes": "^0.1.6-alpha.1",
51
- "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1",
52
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
53
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1",
54
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
55
- "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
56
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.1"
47
+ "@deepseek-ai/cordis": "^4.0.3",
48
+ "@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1",
49
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.7-alpha.1",
50
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-alpha.1",
51
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
52
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-alpha.1",
53
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
54
+ "@deepseek-ai/dsh-package-manifest": "^0.1.7-alpha.1",
55
+ "@deepseek-ai/dsh-client-connection": "^0.1.7-alpha.1",
56
+ "@deepseek-ai/dsh-settings": "^0.1.7-alpha.1",
57
+ "@deepseek-ai/dsh-api-remotes": "^0.1.7-alpha.1"
57
58
  },
58
59
  "dependencies": {
59
- "@deepseek-ai/schemastery": "^3.18.2"
60
+ "@deepseek-ai/schemastery": "^3.18.3"
60
61
  },
61
62
  "files": [
62
63
  "lib/index.js",