@logixjs/i18n 1.0.2-beta.0 → 1.0.2
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/dist/I18n.cjs +13 -127
- package/dist/I18n.cjs.map +1 -1
- package/dist/I18n.d.cts +15 -7
- package/dist/I18n.d.ts +15 -7
- package/dist/I18n.js +1 -3
- package/dist/Token.cjs +42 -36
- package/dist/Token.cjs.map +1 -1
- package/dist/Token.d.cts +6 -7
- package/dist/Token.d.ts +6 -7
- package/dist/Token.js +1 -4
- package/dist/{chunk-4NKGIEWG.js → chunk-DQVWWU7T.js} +14 -20
- package/dist/chunk-DQVWWU7T.js.map +1 -0
- package/dist/chunk-LGNB43KG.js +123 -0
- package/dist/chunk-LGNB43KG.js.map +1 -0
- package/dist/index.cjs +128 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -14
- package/package.json +29 -22
- package/LICENSE +0 -201
- package/dist/I18nModule.cjs +0 -88
- package/dist/I18nModule.cjs.map +0 -1
- package/dist/I18nModule.d.cts +0 -5
- package/dist/I18nModule.d.ts +0 -5
- package/dist/I18nModule.js +0 -9
- package/dist/I18nModule.js.map +0 -1
- package/dist/chunk-4NKGIEWG.js.map +0 -1
- package/dist/chunk-FDPDEDU7.js +0 -1
- package/dist/chunk-FDPDEDU7.js.map +0 -1
- package/dist/chunk-LW6LHDDL.js +0 -1
- package/dist/chunk-LW6LHDDL.js.map +0 -1
- package/dist/chunk-NWWL4MNH.js +0 -116
- package/dist/chunk-NWWL4MNH.js.map +0 -1
- package/dist/chunk-X2U6SCIR.js +0 -45
- package/dist/chunk-X2U6SCIR.js.map +0 -1
package/dist/I18n.cjs
CHANGED
|
@@ -28,118 +28,6 @@ module.exports = __toCommonJS(I18n_exports);
|
|
|
28
28
|
|
|
29
29
|
// src/internal/driver/i18n.ts
|
|
30
30
|
var import_effect = require("effect");
|
|
31
|
-
|
|
32
|
-
// src/internal/token/token.ts
|
|
33
|
-
var InvalidI18nMessageTokenError = class extends Error {
|
|
34
|
-
constructor(reason, details, fix) {
|
|
35
|
-
super(`[InvalidI18nMessageTokenError] reason=${reason}`);
|
|
36
|
-
this.reason = reason;
|
|
37
|
-
this.details = details;
|
|
38
|
-
this.fix = fix;
|
|
39
|
-
this.name = "InvalidI18nMessageTokenError";
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
var TOKEN_BUDGET = {
|
|
43
|
-
keyMaxLen: 96,
|
|
44
|
-
optionKeyMaxCount: 8,
|
|
45
|
-
optionValueStringMaxLen: 96
|
|
46
|
-
};
|
|
47
|
-
var LANGUAGE_FROZEN_KEYS = /* @__PURE__ */ new Set(["lng", "lngs"]);
|
|
48
|
-
var DANGEROUS_OPTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
49
|
-
var isJsonPrimitive = (value) => value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string";
|
|
50
|
-
var invalidToken = (reason, details, fix) => {
|
|
51
|
-
throw new InvalidI18nMessageTokenError(reason, details, fix);
|
|
52
|
-
};
|
|
53
|
-
var canonicalizeTokenOptions = (options) => {
|
|
54
|
-
if (!options) return void 0;
|
|
55
|
-
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
56
|
-
invalidToken("optionValueInvalid", { field: "options", actual: typeof options }, [
|
|
57
|
-
"\u8BF7\u4F20\u5165 plain object \u4F5C\u4E3A options\uFF08Record<string, JsonPrimitive>\uFF09\u3002",
|
|
58
|
-
"\u4E0D\u8981\u4F20\u5165\u6570\u7EC4/\u51FD\u6570/\u7C7B\u5B9E\u4F8B\u7B49\u4E0D\u53EF\u5E8F\u5217\u5316\u503C\u3002"
|
|
59
|
-
]);
|
|
60
|
-
}
|
|
61
|
-
const entries = Object.entries(options).filter((p) => p[1] !== void 0);
|
|
62
|
-
if (entries.length === 0) return void 0;
|
|
63
|
-
if (entries.length > TOKEN_BUDGET.optionKeyMaxCount) {
|
|
64
|
-
invalidToken(
|
|
65
|
-
"tooManyOptions",
|
|
66
|
-
{
|
|
67
|
-
field: "options",
|
|
68
|
-
max: TOKEN_BUDGET.optionKeyMaxCount,
|
|
69
|
-
actual: entries.length
|
|
70
|
-
},
|
|
71
|
-
[
|
|
72
|
-
`\u51CF\u5C11 options \u952E\u6570\u91CF\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.optionKeyMaxCount}\uFF09\u3002`,
|
|
73
|
-
"\u628A\u8F83\u957F\u7684\u4FE1\u606F\u632A\u5230 key \u6216 defaultValue\uFF1B\u907F\u514D\u628A\u5927\u5BF9\u8C61\u585E\u8FDB token\u3002"
|
|
74
|
-
]
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
for (const [k, v] of entries) {
|
|
78
|
-
if (DANGEROUS_OPTION_KEYS.has(k) || k.length === 0) {
|
|
79
|
-
invalidToken("optionKeyInvalid", { field: `options.${k}`, key: k }, [
|
|
80
|
-
"\u8BF7\u4F7F\u7528\u666E\u901A\u5B57\u6BB5\u540D\u4F5C\u4E3A options key\uFF08\u907F\u514D __proto__/constructor/prototype \u7B49\u5371\u9669\u952E\uFF09\u3002",
|
|
81
|
-
"\u5982\u9700\u4F20\u9012\u590D\u6742\u7ED3\u6784\uFF0C\u8BF7\u5148\u5728\u5C55\u793A\u8FB9\u754C\u8F6C\u6362\u4E3A\u5B57\u7B26\u4E32\u3002"
|
|
82
|
-
]);
|
|
83
|
-
}
|
|
84
|
-
if (LANGUAGE_FROZEN_KEYS.has(k)) {
|
|
85
|
-
invalidToken("languageFrozen", { field: `options.${k}`, key: k }, [
|
|
86
|
-
"\u4E0D\u8981\u5728 token options \u4E2D\u4F20\u5165 lng/lngs \u7B49\u8BED\u8A00\u51BB\u7ED3\u5B57\u6BB5\u3002",
|
|
87
|
-
"\u8BED\u8A00\u7531\u5916\u90E8 i18n \u5B9E\u4F8B\u51B3\u5B9A\uFF1Btoken \u53EA\u8868\u8FBE\u201C\u8981\u7FFB\u8BD1\u4EC0\u4E48\u201D\u3002"
|
|
88
|
-
]);
|
|
89
|
-
}
|
|
90
|
-
if (!isJsonPrimitive(v)) {
|
|
91
|
-
invalidToken("optionValueInvalid", { field: `options.${k}`, actual: typeof v }, [
|
|
92
|
-
"options value \u53EA\u5141\u8BB8 JsonPrimitive\uFF08null/boolean/number/string\uFF09\u3002",
|
|
93
|
-
"\u4E0D\u8981\u4F20\u5165\u5BF9\u8C61/\u6570\u7EC4/\u51FD\u6570\uFF1B\u9700\u8981\u65F6\u8BF7\u5728\u5C55\u793A\u8FB9\u754C\u5148\u683C\u5F0F\u5316\u6210\u5B57\u7B26\u4E32\u3002"
|
|
94
|
-
]);
|
|
95
|
-
}
|
|
96
|
-
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
97
|
-
invalidToken("numberNotJsonSafe", { field: `options.${k}`, value: String(v) }, [
|
|
98
|
-
"\u4E0D\u8981\u5728 token options \u4E2D\u4F20\u5165 NaN/Infinity\u3002",
|
|
99
|
-
"\u8BF7\u5148\u628A\u8BE5\u6570\u503C\u8F6C\u6362\u4E3A\u53EF JSON \u5316\u7684 number \u6216 string\u3002"
|
|
100
|
-
]);
|
|
101
|
-
}
|
|
102
|
-
if (typeof v === "string" && v.length > TOKEN_BUDGET.optionValueStringMaxLen) {
|
|
103
|
-
invalidToken(
|
|
104
|
-
"optionValueTooLong",
|
|
105
|
-
{
|
|
106
|
-
field: `options.${k}`,
|
|
107
|
-
maxLen: TOKEN_BUDGET.optionValueStringMaxLen,
|
|
108
|
-
actualLen: v.length
|
|
109
|
-
},
|
|
110
|
-
[
|
|
111
|
-
`\u7F29\u77ED\u5B57\u7B26\u4E32\u503C\u957F\u5EA6\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.optionValueStringMaxLen}\uFF09\u3002`,
|
|
112
|
-
"\u628A\u957F\u6587\u672C\u79FB\u5230 defaultValue \u6216\u76F4\u63A5\u5728\u5C55\u793A\u8FB9\u754C\u751F\u6210\u6700\u7EC8\u5B57\u7B26\u4E32\u3002"
|
|
113
|
-
]
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
118
|
-
const out = {};
|
|
119
|
-
for (const [k, v] of entries) {
|
|
120
|
-
out[k] = v;
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
};
|
|
124
|
-
var token = (key, options) => {
|
|
125
|
-
if (key.length > TOKEN_BUDGET.keyMaxLen) {
|
|
126
|
-
invalidToken("keyTooLong", { field: "key", maxLen: TOKEN_BUDGET.keyMaxLen, actualLen: key.length }, [
|
|
127
|
-
`\u7F29\u77ED key\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.keyMaxLen}\uFF09\u3002`,
|
|
128
|
-
"\u5982\u679C key \u8FC7\u957F\uFF0C\u5EFA\u8BAE\u6539\u4E3A\u201C\u7A33\u5B9A key + \u53D8\u91CF options/defaultValue\u201D\u3002"
|
|
129
|
-
]);
|
|
130
|
-
}
|
|
131
|
-
const canon = canonicalizeTokenOptions(options);
|
|
132
|
-
return canon ? {
|
|
133
|
-
_tag: "i18n",
|
|
134
|
-
key,
|
|
135
|
-
options: canon
|
|
136
|
-
} : {
|
|
137
|
-
_tag: "i18n",
|
|
138
|
-
key
|
|
139
|
-
};
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// src/internal/driver/i18n.ts
|
|
143
31
|
var I18nSnapshotSchema = import_effect.Schema.Struct({
|
|
144
32
|
language: import_effect.Schema.String,
|
|
145
33
|
init: import_effect.Schema.Literals(["pending", "ready", "failed"]),
|
|
@@ -209,22 +97,22 @@ var makeI18nService = (driver) => import_effect.Effect.gen(function* () {
|
|
|
209
97
|
yield* update({ init: "ready", language });
|
|
210
98
|
}
|
|
211
99
|
});
|
|
212
|
-
const fallback = (
|
|
213
|
-
const
|
|
100
|
+
const fallback = (message, hints) => hints?.fallback ?? message.key;
|
|
101
|
+
const render = (message, hints) => {
|
|
214
102
|
if (currentSnapshot.init !== "ready") {
|
|
215
|
-
return fallback(
|
|
103
|
+
return fallback(message, hints);
|
|
216
104
|
}
|
|
217
105
|
try {
|
|
218
|
-
return driver.t(key,
|
|
106
|
+
return driver.t(message.key, message.params);
|
|
219
107
|
} catch {
|
|
220
|
-
return fallback(
|
|
108
|
+
return fallback(message, hints);
|
|
221
109
|
}
|
|
222
110
|
};
|
|
223
|
-
const
|
|
111
|
+
const renderReady = (message, hints, timeoutMs) => import_effect.Effect.gen(function* () {
|
|
224
112
|
const cap = timeoutMs ?? 5e3;
|
|
225
113
|
const snap0 = yield* import_effect.SubscriptionRef.get(snapshotRef);
|
|
226
|
-
if (snap0.init === "ready") return
|
|
227
|
-
if (snap0.init === "failed") return fallback(
|
|
114
|
+
if (snap0.init === "ready") return render(message, hints);
|
|
115
|
+
if (snap0.init === "failed") return fallback(message, hints);
|
|
228
116
|
const wait = import_effect.Stream.filter(import_effect.SubscriptionRef.changes(snapshotRef), (s) => s.init !== "pending").pipe(
|
|
229
117
|
import_effect.Stream.runHead,
|
|
230
118
|
import_effect.Effect.timeoutOption(import_effect.Duration.millis(cap)),
|
|
@@ -234,21 +122,19 @@ var makeI18nService = (driver) => import_effect.Effect.gen(function* () {
|
|
|
234
122
|
const snap1 = yield* import_effect.SubscriptionRef.get(snapshotRef);
|
|
235
123
|
if (snap1.init !== "pending") {
|
|
236
124
|
yield* import_effect.Fiber.interrupt(fiber);
|
|
237
|
-
return snap1.init === "ready" ?
|
|
125
|
+
return snap1.init === "ready" ? render(message, hints) : fallback(message, hints);
|
|
238
126
|
}
|
|
239
127
|
const outcome = yield* import_effect.Fiber.join(fiber);
|
|
240
128
|
return import_effect.Option.match(outcome, {
|
|
241
|
-
onNone: () => fallback(
|
|
242
|
-
onSome: (snap) => snap.init === "ready" ?
|
|
129
|
+
onNone: () => fallback(message, hints),
|
|
130
|
+
onSome: (snap) => snap.init === "ready" ? render(message, hints) : fallback(message, hints)
|
|
243
131
|
});
|
|
244
132
|
});
|
|
245
133
|
return {
|
|
246
|
-
instance: driver,
|
|
247
134
|
snapshot: snapshotRef,
|
|
248
|
-
token,
|
|
249
135
|
changeLanguage,
|
|
250
|
-
|
|
251
|
-
|
|
136
|
+
render,
|
|
137
|
+
renderReady
|
|
252
138
|
};
|
|
253
139
|
});
|
|
254
140
|
var I18n = {
|
package/dist/I18n.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/I18n.ts","../src/internal/driver/i18n.ts","../src/internal/token/token.ts"],"sourcesContent":["export type {\n I18nDriver,\n I18nInitState,\n I18nService,\n I18nSnapshot,\n I18nTokenOptionsInput,\n} from './internal/driver/i18n.js'\nexport { I18n, I18nSnapshotSchema, I18nTag } from './internal/driver/i18n.js'\n","import {\n Duration,\n Effect,\n Fiber,\n Layer,\n ManagedRuntime,\n Option,\n ServiceMap,\n Scope,\n Schema,\n Stream,\n SubscriptionRef,\n} from 'effect'\n\nimport type { I18nMessageToken, I18nTokenOptionsInput } from '../token/token.js'\nimport { token } from '../token/token.js'\n\nexport type { I18nTokenOptionsInput } from '../token/token.js'\n\nexport type I18nDriver = {\n readonly language: string\n readonly isInitialized?: boolean\n readonly t: (key: string, options?: unknown) => string\n readonly changeLanguage: (language: string) => Promise<unknown> | unknown\n readonly on: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown\n readonly off: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown\n}\n\nexport type I18nInitState = 'pending' | 'ready' | 'failed'\n\nexport type I18nSnapshot = {\n readonly language: string\n readonly init: I18nInitState\n readonly seq: number\n}\n\nexport const I18nSnapshotSchema = Schema.Struct({\n language: Schema.String,\n init: Schema.Literals(['pending', 'ready', 'failed']),\n seq: Schema.Number,\n})\n\nexport type I18nService = {\n readonly instance: I18nDriver\n readonly snapshot: SubscriptionRef.SubscriptionRef<I18nSnapshot>\n readonly token: (key: string, options?: I18nTokenOptionsInput) => I18nMessageToken\n readonly changeLanguage: (language: string) => Effect.Effect<void, never, never>\n readonly t: (key: string, options?: I18nTokenOptionsInput) => string\n readonly tReady: (\n key: string,\n options?: I18nTokenOptionsInput,\n timeoutMs?: number,\n ) => Effect.Effect<string, never, never>\n}\n\nexport class I18nTag extends ServiceMap.Service<I18nTag, I18nService>()('@logixjs/i18n/I18n') {}\n\nconst asNonEmptyString = (value: unknown): string | undefined =>\n typeof value === 'string' && value.length > 0 ? value : undefined\n\nconst makeI18nService = (driver: I18nDriver): Effect.Effect<I18nService, never, Scope.Scope> =>\n Effect.gen(function* () {\n const init: I18nInitState = driver.isInitialized ? 'ready' : 'pending'\n let currentSnapshot: I18nSnapshot = {\n language: driver.language,\n init,\n seq: 0,\n }\n const snapshotRef = yield* SubscriptionRef.make<I18nSnapshot>(currentSnapshot)\n\n const update = (patch: Partial<Pick<I18nSnapshot, 'language' | 'init'>>): Effect.Effect<void> =>\n SubscriptionRef.update(snapshotRef, (prev) => {\n const next: I18nSnapshot = {\n language: patch.language ?? prev.language,\n init: patch.init ?? prev.init,\n seq: prev.seq + 1,\n }\n currentSnapshot = next\n return next\n })\n\n const pushSnapshot = (patch: Partial<Pick<I18nSnapshot, 'language' | 'init'>>): void => {\n const next: I18nSnapshot = {\n language: patch.language ?? currentSnapshot.language,\n init: patch.init ?? currentSnapshot.init,\n seq: currentSnapshot.seq + 1,\n }\n currentSnapshot = next\n void Effect.runPromise(SubscriptionRef.set(snapshotRef, next).pipe(Effect.catchCause(() => Effect.void)))\n }\n\n const onInitialized = (): void => {\n pushSnapshot({ init: 'ready', language: driver.language })\n }\n\n const onLanguageChanged = (lang: unknown): void => {\n pushSnapshot({\n init: 'ready',\n language: asNonEmptyString(lang) ?? driver.language,\n })\n }\n\n yield* Effect.sync(() => {\n driver.on('initialized', onInitialized)\n driver.on('languageChanged', onLanguageChanged)\n })\n\n const scope = yield* Effect.scope\n yield* Scope.addFinalizer(\n scope,\n Effect.sync(() => {\n driver.off('initialized', onInitialized)\n driver.off('languageChanged', onLanguageChanged)\n }),\n )\n\n const changeLanguage = (language: string): Effect.Effect<void, never, never> =>\n Effect.gen(function* () {\n yield* update({ language, init: 'pending' })\n const exit = yield* Effect.exit(\n Effect.tryPromise({\n try: () => Promise.resolve(driver.changeLanguage(language)),\n catch: () => undefined,\n }),\n )\n if (exit._tag === 'Failure') {\n yield* update({ init: 'failed' })\n } else {\n yield* update({ init: 'ready', language })\n }\n })\n\n const fallback = (key: string, options?: I18nTokenOptionsInput): string =>\n typeof options?.defaultValue === 'string' ? options.defaultValue : key\n\n const t = (key: string, options?: I18nTokenOptionsInput): string => {\n if (currentSnapshot.init !== 'ready') {\n return fallback(key, options)\n }\n try {\n return driver.t(key, options)\n } catch {\n return fallback(key, options)\n }\n }\n\n const tReady = (\n key: string,\n options?: I18nTokenOptionsInput,\n timeoutMs?: number,\n ): Effect.Effect<string, never, never> =>\n Effect.gen(function* () {\n const cap = timeoutMs ?? 5000\n const snap0 = yield* SubscriptionRef.get(snapshotRef)\n if (snap0.init === 'ready') return t(key, options)\n if (snap0.init === 'failed') return fallback(key, options)\n\n const wait = Stream.filter(SubscriptionRef.changes(snapshotRef), (s) => s.init !== 'pending').pipe(\n Stream.runHead,\n Effect.timeoutOption(Duration.millis(cap)),\n Effect.map((maybe) => (Option.isSome(maybe) ? maybe.value : Option.none())),\n )\n\n const fiber = yield* wait.pipe(Effect.forkChild)\n\n const snap1 = yield* SubscriptionRef.get(snapshotRef)\n if (snap1.init !== 'pending') {\n yield* Fiber.interrupt(fiber)\n return snap1.init === 'ready' ? t(key, options) : fallback(key, options)\n }\n\n const outcome = yield* Fiber.join(fiber)\n return Option.match(outcome, {\n onNone: () => fallback(key, options),\n onSome: (snap) => (snap.init === 'ready' ? t(key, options) : fallback(key, options)),\n })\n })\n\n return {\n instance: driver,\n snapshot: snapshotRef,\n token,\n changeLanguage,\n t,\n tReady,\n } as const\n })\n\nexport const I18n = {\n layer: (driver: I18nDriver): Layer.Layer<I18nTag, never, never> => Layer.effect(I18nTag, makeI18nService(driver)),\n} as const\n","export type JsonPrimitive = null | boolean | number | string\n\nexport type I18nTokenOptions = Readonly<Record<string, JsonPrimitive>>\nexport type I18nTokenOptionsInput = Readonly<Record<string, JsonPrimitive | undefined>>\n\nexport type I18nMessageToken = {\n readonly _tag: 'i18n'\n readonly key: string\n readonly options?: I18nTokenOptions\n}\n\nexport type InvalidI18nMessageTokenReason =\n | 'keyTooLong'\n | 'tooManyOptions'\n | 'optionKeyInvalid'\n | 'optionValueInvalid'\n | 'optionValueTooLong'\n | 'numberNotJsonSafe'\n | 'languageFrozen'\n\nexport class InvalidI18nMessageTokenError extends Error {\n readonly name = 'InvalidI18nMessageTokenError'\n\n constructor(\n readonly reason: InvalidI18nMessageTokenReason,\n readonly details: unknown,\n readonly fix: ReadonlyArray<string>,\n ) {\n super(`[InvalidI18nMessageTokenError] reason=${reason}`)\n }\n}\n\nconst TOKEN_BUDGET = {\n keyMaxLen: 96,\n optionKeyMaxCount: 8,\n optionValueStringMaxLen: 96,\n} as const\n\nconst LANGUAGE_FROZEN_KEYS = new Set(['lng', 'lngs'])\nconst DANGEROUS_OPTION_KEYS = new Set(['__proto__', 'prototype', 'constructor'])\n\nconst isJsonPrimitive = (value: unknown): value is JsonPrimitive =>\n value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string'\n\nconst invalidToken = (reason: InvalidI18nMessageTokenReason, details: unknown, fix: ReadonlyArray<string>): never => {\n throw new InvalidI18nMessageTokenError(reason, details, fix)\n}\n\nexport const canonicalizeTokenOptions = (options: I18nTokenOptionsInput | undefined): I18nTokenOptions | undefined => {\n if (!options) return undefined\n if (typeof options !== 'object' || options === null || Array.isArray(options)) {\n invalidToken('optionValueInvalid', { field: 'options', actual: typeof options }, [\n '请传入 plain object 作为 options(Record<string, JsonPrimitive>)。',\n '不要传入数组/函数/类实例等不可序列化值。',\n ])\n }\n\n const entries = Object.entries(options).filter((p): p is [string, JsonPrimitive] => p[1] !== undefined)\n\n if (entries.length === 0) return undefined\n\n if (entries.length > TOKEN_BUDGET.optionKeyMaxCount) {\n invalidToken(\n 'tooManyOptions',\n {\n field: 'options',\n max: TOKEN_BUDGET.optionKeyMaxCount,\n actual: entries.length,\n },\n [\n `减少 options 键数量(建议 ≤ ${TOKEN_BUDGET.optionKeyMaxCount})。`,\n '把较长的信息挪到 key 或 defaultValue;避免把大对象塞进 token。',\n ],\n )\n }\n\n for (const [k, v] of entries) {\n if (DANGEROUS_OPTION_KEYS.has(k) || k.length === 0) {\n invalidToken('optionKeyInvalid', { field: `options.${k}`, key: k }, [\n '请使用普通字段名作为 options key(避免 __proto__/constructor/prototype 等危险键)。',\n '如需传递复杂结构,请先在展示边界转换为字符串。',\n ])\n }\n\n if (LANGUAGE_FROZEN_KEYS.has(k)) {\n invalidToken('languageFrozen', { field: `options.${k}`, key: k }, [\n '不要在 token options 中传入 lng/lngs 等语言冻结字段。',\n '语言由外部 i18n 实例决定;token 只表达“要翻译什么”。',\n ])\n }\n\n if (!isJsonPrimitive(v)) {\n invalidToken('optionValueInvalid', { field: `options.${k}`, actual: typeof v }, [\n 'options value 只允许 JsonPrimitive(null/boolean/number/string)。',\n '不要传入对象/数组/函数;需要时请在展示边界先格式化成字符串。',\n ])\n }\n\n if (typeof v === 'number' && !Number.isFinite(v)) {\n invalidToken('numberNotJsonSafe', { field: `options.${k}`, value: String(v) }, [\n '不要在 token options 中传入 NaN/Infinity。',\n '请先把该数值转换为可 JSON 化的 number 或 string。',\n ])\n }\n\n if (typeof v === 'string' && v.length > TOKEN_BUDGET.optionValueStringMaxLen) {\n invalidToken(\n 'optionValueTooLong',\n {\n field: `options.${k}`,\n maxLen: TOKEN_BUDGET.optionValueStringMaxLen,\n actualLen: v.length,\n },\n [\n `缩短字符串值长度(建议 ≤ ${TOKEN_BUDGET.optionValueStringMaxLen})。`,\n '把长文本移到 defaultValue 或直接在展示边界生成最终字符串。',\n ],\n )\n }\n }\n\n entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\n const out: Record<string, JsonPrimitive> = {}\n for (const [k, v] of entries) {\n out[k] = v\n }\n return out\n}\n\nexport const token = (key: string, options?: I18nTokenOptionsInput): I18nMessageToken => {\n if (key.length > TOKEN_BUDGET.keyMaxLen) {\n invalidToken('keyTooLong', { field: 'key', maxLen: TOKEN_BUDGET.keyMaxLen, actualLen: key.length }, [\n `缩短 key(建议 ≤ ${TOKEN_BUDGET.keyMaxLen})。`,\n '如果 key 过长,建议改为“稳定 key + 变量 options/defaultValue”。',\n ])\n }\n\n const canon = canonicalizeTokenOptions(options)\n return canon\n ? {\n _tag: 'i18n',\n key,\n options: canon,\n }\n : {\n _tag: 'i18n',\n key,\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/I18n.ts","../src/internal/driver/i18n.ts"],"sourcesContent":["export type {\n I18nDriver,\n I18nInitState,\n I18nRenderHints,\n I18nService,\n I18nSnapshot,\n I18nTokenParamsInput,\n} from './internal/driver/i18n.js'\n\nexport { I18n, I18nSnapshotSchema, I18nTag } from './internal/driver/i18n.js'\n","import {\n Duration,\n Effect,\n Fiber,\n Layer,\n ManagedRuntime,\n Option,\n ServiceMap,\n Scope,\n Schema,\n Stream,\n SubscriptionRef,\n} from 'effect'\n\nimport type { I18nMessageToken, I18nTokenParamsInput } from '../token/token.js'\n\nexport type { I18nTokenParamsInput } from '../token/token.js'\n\n/**\n * Low-level driver contract.\n *\n * Package-level canonical consumption remains service-first:\n * callers should read `services.i18n` or `I18nTag`, rather than coupling to driver lifecycle details directly.\n */\nexport type I18nDriver = {\n readonly language: string\n readonly isInitialized?: boolean\n readonly t: (key: string, input?: unknown) => string\n readonly changeLanguage: (language: string) => Promise<unknown> | unknown\n readonly on: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown\n readonly off: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown\n}\n\nexport type I18nInitState = 'pending' | 'ready' | 'failed'\n\nexport type I18nSnapshot = {\n readonly language: string\n readonly init: I18nInitState\n readonly seq: number\n}\n\nexport const I18nSnapshotSchema = Schema.Struct({\n language: Schema.String,\n init: Schema.Literals(['pending', 'ready', 'failed']),\n seq: Schema.Number,\n})\n\nexport type I18nRenderHints = {\n readonly fallback?: string\n}\n\nexport type I18nService = {\n readonly snapshot: SubscriptionRef.SubscriptionRef<I18nSnapshot>\n readonly changeLanguage: (language: string) => Effect.Effect<void, never, never>\n readonly render: (token: I18nMessageToken, hints?: I18nRenderHints) => string\n readonly renderReady: (\n token: I18nMessageToken,\n hints?: I18nRenderHints,\n timeoutMs?: number,\n ) => Effect.Effect<string, never, never>\n}\n\nexport class I18nTag extends ServiceMap.Service<I18nTag, I18nService>()('@logixjs/i18n/I18n') {}\n\nconst asNonEmptyString = (value: unknown): string | undefined =>\n typeof value === 'string' && value.length > 0 ? value : undefined\n\nconst makeI18nService = (driver: I18nDriver): Effect.Effect<I18nService, never, Scope.Scope> =>\n Effect.gen(function* () {\n const init: I18nInitState = driver.isInitialized ? 'ready' : 'pending'\n let currentSnapshot: I18nSnapshot = {\n language: driver.language,\n init,\n seq: 0,\n }\n const snapshotRef = yield* SubscriptionRef.make<I18nSnapshot>(currentSnapshot)\n\n const update = (patch: Partial<Pick<I18nSnapshot, 'language' | 'init'>>): Effect.Effect<void> =>\n SubscriptionRef.update(snapshotRef, (prev) => {\n const next: I18nSnapshot = {\n language: patch.language ?? prev.language,\n init: patch.init ?? prev.init,\n seq: prev.seq + 1,\n }\n currentSnapshot = next\n return next\n })\n\n const pushSnapshot = (patch: Partial<Pick<I18nSnapshot, 'language' | 'init'>>): void => {\n const next: I18nSnapshot = {\n language: patch.language ?? currentSnapshot.language,\n init: patch.init ?? currentSnapshot.init,\n seq: currentSnapshot.seq + 1,\n }\n currentSnapshot = next\n void Effect.runPromise(SubscriptionRef.set(snapshotRef, next).pipe(Effect.catchCause(() => Effect.void)))\n }\n\n const onInitialized = (): void => {\n pushSnapshot({ init: 'ready', language: driver.language })\n }\n\n const onLanguageChanged = (lang: unknown): void => {\n pushSnapshot({\n init: 'ready',\n language: asNonEmptyString(lang) ?? driver.language,\n })\n }\n\n yield* Effect.sync(() => {\n driver.on('initialized', onInitialized)\n driver.on('languageChanged', onLanguageChanged)\n })\n\n const scope = yield* Effect.scope\n yield* Scope.addFinalizer(\n scope,\n Effect.sync(() => {\n driver.off('initialized', onInitialized)\n driver.off('languageChanged', onLanguageChanged)\n }),\n )\n\n /**\n * Canonical runtime-facing lifecycle wiring:\n * - driver events update the shared snapshot\n * - consumers stay on the service contract (`render`, `renderReady`, `snapshot`)\n */\n const changeLanguage = (language: string): Effect.Effect<void, never, never> =>\n Effect.gen(function* () {\n yield* update({ language, init: 'pending' })\n const exit = yield* Effect.exit(\n Effect.tryPromise({\n try: () => Promise.resolve(driver.changeLanguage(language)),\n catch: () => undefined,\n }),\n )\n if (exit._tag === 'Failure') {\n yield* update({ init: 'failed' })\n } else {\n yield* update({ init: 'ready', language })\n }\n })\n\n const fallback = (message: I18nMessageToken, hints?: I18nRenderHints): string => hints?.fallback ?? message.key\n\n const render = (message: I18nMessageToken, hints?: I18nRenderHints): string => {\n if (currentSnapshot.init !== 'ready') {\n return fallback(message, hints)\n }\n try {\n return driver.t(message.key, message.params as I18nTokenParamsInput | undefined)\n } catch {\n return fallback(message, hints)\n }\n }\n\n const renderReady = (\n message: I18nMessageToken,\n hints?: I18nRenderHints,\n timeoutMs?: number,\n ): Effect.Effect<string, never, never> =>\n Effect.gen(function* () {\n const cap = timeoutMs ?? 5000\n const snap0 = yield* SubscriptionRef.get(snapshotRef)\n if (snap0.init === 'ready') return render(message, hints)\n if (snap0.init === 'failed') return fallback(message, hints)\n\n const wait = Stream.filter(SubscriptionRef.changes(snapshotRef), (s) => s.init !== 'pending').pipe(\n Stream.runHead,\n Effect.timeoutOption(Duration.millis(cap)),\n Effect.map((maybe) => (Option.isSome(maybe) ? maybe.value : Option.none())),\n )\n\n const fiber = yield* wait.pipe(Effect.forkChild)\n\n const snap1 = yield* SubscriptionRef.get(snapshotRef)\n if (snap1.init !== 'pending') {\n yield* Fiber.interrupt(fiber)\n return snap1.init === 'ready' ? render(message, hints) : fallback(message, hints)\n }\n\n const outcome = yield* Fiber.join(fiber)\n return Option.match(outcome, {\n onNone: () => fallback(message, hints),\n onSome: (snap) => (snap.init === 'ready' ? render(message, hints) : fallback(message, hints)),\n })\n })\n\n return {\n snapshot: snapshotRef,\n changeLanguage,\n render,\n renderReady,\n } as const\n })\n\nexport const I18n = {\n layer: (driver: I18nDriver): Layer.Layer<I18nTag, never, never> => Layer.effect(I18nTag, makeI18nService(driver)),\n} as const\n"],"mappings":"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","names":[]}
|
package/dist/I18n.d.cts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { I18nMessageToken } from './Token.cjs';
|
|
2
|
+
export { I18nTokenParamsInput } from './Token.cjs';
|
|
1
3
|
import { SubscriptionRef, Effect, Layer, ServiceMap, Schema } from 'effect';
|
|
2
|
-
import { I18nTokenOptionsInput, I18nMessageToken } from './Token.cjs';
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Low-level driver contract.
|
|
7
|
+
*
|
|
8
|
+
* Package-level canonical consumption remains service-first:
|
|
9
|
+
* callers should read `services.i18n` or `I18nTag`, rather than coupling to driver lifecycle details directly.
|
|
10
|
+
*/
|
|
4
11
|
type I18nDriver = {
|
|
5
12
|
readonly language: string;
|
|
6
13
|
readonly isInitialized?: boolean;
|
|
7
|
-
readonly t: (key: string,
|
|
14
|
+
readonly t: (key: string, input?: unknown) => string;
|
|
8
15
|
readonly changeLanguage: (language: string) => Promise<unknown> | unknown;
|
|
9
16
|
readonly on: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown;
|
|
10
17
|
readonly off: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown;
|
|
@@ -20,13 +27,14 @@ declare const I18nSnapshotSchema: Schema.Struct<{
|
|
|
20
27
|
readonly init: Schema.Literals<readonly ["pending", "ready", "failed"]>;
|
|
21
28
|
readonly seq: Schema.Number;
|
|
22
29
|
}>;
|
|
30
|
+
type I18nRenderHints = {
|
|
31
|
+
readonly fallback?: string;
|
|
32
|
+
};
|
|
23
33
|
type I18nService = {
|
|
24
|
-
readonly instance: I18nDriver;
|
|
25
34
|
readonly snapshot: SubscriptionRef.SubscriptionRef<I18nSnapshot>;
|
|
26
|
-
readonly token: (key: string, options?: I18nTokenOptionsInput) => I18nMessageToken;
|
|
27
35
|
readonly changeLanguage: (language: string) => Effect.Effect<void, never, never>;
|
|
28
|
-
readonly
|
|
29
|
-
readonly
|
|
36
|
+
readonly render: (token: I18nMessageToken, hints?: I18nRenderHints) => string;
|
|
37
|
+
readonly renderReady: (token: I18nMessageToken, hints?: I18nRenderHints, timeoutMs?: number) => Effect.Effect<string, never, never>;
|
|
30
38
|
};
|
|
31
39
|
declare const I18nTag_base: ServiceMap.ServiceClass<I18nTag, "@logixjs/i18n/I18n", I18nService>;
|
|
32
40
|
declare class I18nTag extends I18nTag_base {
|
|
@@ -35,4 +43,4 @@ declare const I18n: {
|
|
|
35
43
|
readonly layer: (driver: I18nDriver) => Layer.Layer<I18nTag, never, never>;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
export { I18n, type I18nDriver, type I18nInitState, type I18nService, type I18nSnapshot, I18nSnapshotSchema, I18nTag
|
|
46
|
+
export { I18n, type I18nDriver, type I18nInitState, type I18nRenderHints, type I18nService, type I18nSnapshot, I18nSnapshotSchema, I18nTag };
|
package/dist/I18n.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { I18nMessageToken } from './Token.js';
|
|
2
|
+
export { I18nTokenParamsInput } from './Token.js';
|
|
1
3
|
import { SubscriptionRef, Effect, Layer, ServiceMap, Schema } from 'effect';
|
|
2
|
-
import { I18nTokenOptionsInput, I18nMessageToken } from './Token.js';
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Low-level driver contract.
|
|
7
|
+
*
|
|
8
|
+
* Package-level canonical consumption remains service-first:
|
|
9
|
+
* callers should read `services.i18n` or `I18nTag`, rather than coupling to driver lifecycle details directly.
|
|
10
|
+
*/
|
|
4
11
|
type I18nDriver = {
|
|
5
12
|
readonly language: string;
|
|
6
13
|
readonly isInitialized?: boolean;
|
|
7
|
-
readonly t: (key: string,
|
|
14
|
+
readonly t: (key: string, input?: unknown) => string;
|
|
8
15
|
readonly changeLanguage: (language: string) => Promise<unknown> | unknown;
|
|
9
16
|
readonly on: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown;
|
|
10
17
|
readonly off: (event: 'initialized' | 'languageChanged', handler: (...args: any[]) => void) => unknown;
|
|
@@ -20,13 +27,14 @@ declare const I18nSnapshotSchema: Schema.Struct<{
|
|
|
20
27
|
readonly init: Schema.Literals<readonly ["pending", "ready", "failed"]>;
|
|
21
28
|
readonly seq: Schema.Number;
|
|
22
29
|
}>;
|
|
30
|
+
type I18nRenderHints = {
|
|
31
|
+
readonly fallback?: string;
|
|
32
|
+
};
|
|
23
33
|
type I18nService = {
|
|
24
|
-
readonly instance: I18nDriver;
|
|
25
34
|
readonly snapshot: SubscriptionRef.SubscriptionRef<I18nSnapshot>;
|
|
26
|
-
readonly token: (key: string, options?: I18nTokenOptionsInput) => I18nMessageToken;
|
|
27
35
|
readonly changeLanguage: (language: string) => Effect.Effect<void, never, never>;
|
|
28
|
-
readonly
|
|
29
|
-
readonly
|
|
36
|
+
readonly render: (token: I18nMessageToken, hints?: I18nRenderHints) => string;
|
|
37
|
+
readonly renderReady: (token: I18nMessageToken, hints?: I18nRenderHints, timeoutMs?: number) => Effect.Effect<string, never, never>;
|
|
30
38
|
};
|
|
31
39
|
declare const I18nTag_base: ServiceMap.ServiceClass<I18nTag, "@logixjs/i18n/I18n", I18nService>;
|
|
32
40
|
declare class I18nTag extends I18nTag_base {
|
|
@@ -35,4 +43,4 @@ declare const I18n: {
|
|
|
35
43
|
readonly layer: (driver: I18nDriver) => Layer.Layer<I18nTag, never, never>;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
export { I18n, type I18nDriver, type I18nInitState, type I18nService, type I18nSnapshot, I18nSnapshotSchema, I18nTag
|
|
46
|
+
export { I18n, type I18nDriver, type I18nInitState, type I18nRenderHints, type I18nService, type I18nSnapshot, I18nSnapshotSchema, I18nTag };
|
package/dist/I18n.js
CHANGED
package/dist/Token.cjs
CHANGED
|
@@ -21,7 +21,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var Token_exports = {};
|
|
22
22
|
__export(Token_exports, {
|
|
23
23
|
InvalidI18nMessageTokenError: () => InvalidI18nMessageTokenError,
|
|
24
|
-
canonicalizeTokenOptions: () => canonicalizeTokenOptions,
|
|
25
24
|
token: () => token
|
|
26
25
|
});
|
|
27
26
|
module.exports = __toCommonJS(Token_exports);
|
|
@@ -38,75 +37,83 @@ var InvalidI18nMessageTokenError = class extends Error {
|
|
|
38
37
|
};
|
|
39
38
|
var TOKEN_BUDGET = {
|
|
40
39
|
keyMaxLen: 96,
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
paramKeyMaxCount: 8,
|
|
41
|
+
paramValueStringMaxLen: 96
|
|
43
42
|
};
|
|
44
43
|
var LANGUAGE_FROZEN_KEYS = /* @__PURE__ */ new Set(["lng", "lngs"]);
|
|
45
|
-
var
|
|
44
|
+
var DANGEROUS_PARAM_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
45
|
+
var LEGACY_RENDER_FALLBACK_KEY = `default${"Value"}`;
|
|
46
|
+
var RESERVED_RENDER_FALLBACK_KEYS = /* @__PURE__ */ new Set([LEGACY_RENDER_FALLBACK_KEY]);
|
|
46
47
|
var isJsonPrimitive = (value) => value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string";
|
|
47
48
|
var invalidToken = (reason, details, fix) => {
|
|
48
49
|
throw new InvalidI18nMessageTokenError(reason, details, fix);
|
|
49
50
|
};
|
|
50
|
-
var
|
|
51
|
-
if (!
|
|
52
|
-
if (typeof
|
|
53
|
-
invalidToken("
|
|
54
|
-
"\u8BF7\u4F20\u5165 plain object \u4F5C\u4E3A
|
|
51
|
+
var canonicalizeTokenParams = (params) => {
|
|
52
|
+
if (!params) return void 0;
|
|
53
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
54
|
+
invalidToken("paramValueInvalid", { field: "params", actual: typeof params }, [
|
|
55
|
+
"\u8BF7\u4F20\u5165 plain object \u4F5C\u4E3A params\uFF08Record<string, JsonPrimitive>\uFF09\u3002",
|
|
55
56
|
"\u4E0D\u8981\u4F20\u5165\u6570\u7EC4/\u51FD\u6570/\u7C7B\u5B9E\u4F8B\u7B49\u4E0D\u53EF\u5E8F\u5217\u5316\u503C\u3002"
|
|
56
57
|
]);
|
|
57
58
|
}
|
|
58
|
-
const entries = Object.entries(
|
|
59
|
+
const entries = Object.entries(params).filter((p) => p[1] !== void 0);
|
|
59
60
|
if (entries.length === 0) return void 0;
|
|
60
|
-
if (entries.length > TOKEN_BUDGET.
|
|
61
|
+
if (entries.length > TOKEN_BUDGET.paramKeyMaxCount) {
|
|
61
62
|
invalidToken(
|
|
62
|
-
"
|
|
63
|
+
"tooManyParams",
|
|
63
64
|
{
|
|
64
|
-
field: "
|
|
65
|
-
max: TOKEN_BUDGET.
|
|
65
|
+
field: "params",
|
|
66
|
+
max: TOKEN_BUDGET.paramKeyMaxCount,
|
|
66
67
|
actual: entries.length
|
|
67
68
|
},
|
|
68
69
|
[
|
|
69
|
-
`\u51CF\u5C11
|
|
70
|
-
"\
|
|
70
|
+
`\u51CF\u5C11 params \u952E\u6570\u91CF\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.paramKeyMaxCount}\uFF09\u3002`,
|
|
71
|
+
"\u907F\u514D\u628A\u5927\u5BF9\u8C61\u6216\u5C55\u793A\u515C\u5E95\u6587\u6848\u585E\u8FDB semantic token\u3002"
|
|
71
72
|
]
|
|
72
73
|
);
|
|
73
74
|
}
|
|
74
75
|
for (const [k, v] of entries) {
|
|
75
|
-
if (
|
|
76
|
-
invalidToken("
|
|
77
|
-
"\
|
|
76
|
+
if (RESERVED_RENDER_FALLBACK_KEYS.has(k)) {
|
|
77
|
+
invalidToken("renderFallbackReserved", { field: `params.${k}`, key: k }, [
|
|
78
|
+
"\u4E0D\u8981\u628A\u65E7\u7684\u5C55\u793A\u515C\u5E95\u5B57\u6BB5\u653E\u8FDB semantic token params\u3002",
|
|
79
|
+
"\u8BF7\u628A\u515C\u5E95\u6587\u6848\u6539\u653E\u5230 render/renderReady \u7684 hints.fallback\u3002"
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
if (DANGEROUS_PARAM_KEYS.has(k) || k.length === 0) {
|
|
83
|
+
invalidToken("paramKeyInvalid", { field: `params.${k}`, key: k }, [
|
|
84
|
+
"\u8BF7\u4F7F\u7528\u666E\u901A\u5B57\u6BB5\u540D\u4F5C\u4E3A params key\uFF0C\u907F\u514D __proto__/constructor/prototype \u7B49\u5371\u9669\u952E\u3002",
|
|
78
85
|
"\u5982\u9700\u4F20\u9012\u590D\u6742\u7ED3\u6784\uFF0C\u8BF7\u5148\u5728\u5C55\u793A\u8FB9\u754C\u8F6C\u6362\u4E3A\u5B57\u7B26\u4E32\u3002"
|
|
79
86
|
]);
|
|
80
87
|
}
|
|
81
88
|
if (LANGUAGE_FROZEN_KEYS.has(k)) {
|
|
82
|
-
invalidToken("languageFrozen", { field: `
|
|
83
|
-
"\u4E0D\u8981\u5728 token
|
|
89
|
+
invalidToken("languageFrozen", { field: `params.${k}`, key: k }, [
|
|
90
|
+
"\u4E0D\u8981\u5728 token params \u4E2D\u4F20\u5165 lng/lngs \u7B49\u8BED\u8A00\u51BB\u7ED3\u5B57\u6BB5\u3002",
|
|
84
91
|
"\u8BED\u8A00\u7531\u5916\u90E8 i18n \u5B9E\u4F8B\u51B3\u5B9A\uFF1Btoken \u53EA\u8868\u8FBE\u201C\u8981\u7FFB\u8BD1\u4EC0\u4E48\u201D\u3002"
|
|
85
92
|
]);
|
|
86
93
|
}
|
|
87
94
|
if (!isJsonPrimitive(v)) {
|
|
88
|
-
invalidToken("
|
|
89
|
-
"
|
|
95
|
+
invalidToken("paramValueInvalid", { field: `params.${k}`, actual: typeof v }, [
|
|
96
|
+
"params value \u53EA\u5141\u8BB8 JsonPrimitive\uFF08null/boolean/number/string\uFF09\u3002",
|
|
90
97
|
"\u4E0D\u8981\u4F20\u5165\u5BF9\u8C61/\u6570\u7EC4/\u51FD\u6570\uFF1B\u9700\u8981\u65F6\u8BF7\u5728\u5C55\u793A\u8FB9\u754C\u5148\u683C\u5F0F\u5316\u6210\u5B57\u7B26\u4E32\u3002"
|
|
91
98
|
]);
|
|
92
99
|
}
|
|
93
100
|
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
94
|
-
invalidToken("numberNotJsonSafe", { field: `
|
|
95
|
-
"\u4E0D\u8981\u5728 token
|
|
101
|
+
invalidToken("numberNotJsonSafe", { field: `params.${k}`, value: String(v) }, [
|
|
102
|
+
"\u4E0D\u8981\u5728 token params \u4E2D\u4F20\u5165 NaN/Infinity\u3002",
|
|
96
103
|
"\u8BF7\u5148\u628A\u8BE5\u6570\u503C\u8F6C\u6362\u4E3A\u53EF JSON \u5316\u7684 number \u6216 string\u3002"
|
|
97
104
|
]);
|
|
98
105
|
}
|
|
99
|
-
if (typeof v === "string" && v.length > TOKEN_BUDGET.
|
|
106
|
+
if (typeof v === "string" && v.length > TOKEN_BUDGET.paramValueStringMaxLen) {
|
|
100
107
|
invalidToken(
|
|
101
|
-
"
|
|
108
|
+
"paramValueTooLong",
|
|
102
109
|
{
|
|
103
|
-
field: `
|
|
104
|
-
maxLen: TOKEN_BUDGET.
|
|
110
|
+
field: `params.${k}`,
|
|
111
|
+
maxLen: TOKEN_BUDGET.paramValueStringMaxLen,
|
|
105
112
|
actualLen: v.length
|
|
106
113
|
},
|
|
107
114
|
[
|
|
108
|
-
`\u7F29\u77ED\u5B57\u7B26\u4E32\u503C\u957F\u5EA6\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.
|
|
109
|
-
"\u628A\u957F\u6587\u672C\
|
|
115
|
+
`\u7F29\u77ED\u5B57\u7B26\u4E32\u503C\u957F\u5EA6\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.paramValueStringMaxLen}\uFF09\u3002`,
|
|
116
|
+
"\u628A\u957F\u6587\u672C\u7559\u5728\u5C55\u793A\u8FB9\u754C\uFF0C\u4E0D\u8981\u653E\u8FDB semantic token\u3002"
|
|
110
117
|
]
|
|
111
118
|
);
|
|
112
119
|
}
|
|
@@ -118,18 +125,18 @@ var canonicalizeTokenOptions = (options) => {
|
|
|
118
125
|
}
|
|
119
126
|
return out;
|
|
120
127
|
};
|
|
121
|
-
var token = (key,
|
|
128
|
+
var token = (key, params) => {
|
|
122
129
|
if (key.length > TOKEN_BUDGET.keyMaxLen) {
|
|
123
130
|
invalidToken("keyTooLong", { field: "key", maxLen: TOKEN_BUDGET.keyMaxLen, actualLen: key.length }, [
|
|
124
131
|
`\u7F29\u77ED key\uFF08\u5EFA\u8BAE \u2264 ${TOKEN_BUDGET.keyMaxLen}\uFF09\u3002`,
|
|
125
|
-
"\u5982\u679C key \u8FC7\u957F\uFF0C\u5EFA\u8BAE\u6539\u4E3A\u201C\u7A33\u5B9A key + \
|
|
132
|
+
"\u5982\u679C key \u8FC7\u957F\uFF0C\u5EFA\u8BAE\u6539\u4E3A\u201C\u7A33\u5B9A key + \u5C11\u91CF semantic params\u201D\u3002"
|
|
126
133
|
]);
|
|
127
134
|
}
|
|
128
|
-
const canon =
|
|
135
|
+
const canon = canonicalizeTokenParams(params);
|
|
129
136
|
return canon ? {
|
|
130
137
|
_tag: "i18n",
|
|
131
138
|
key,
|
|
132
|
-
|
|
139
|
+
params: canon
|
|
133
140
|
} : {
|
|
134
141
|
_tag: "i18n",
|
|
135
142
|
key
|
|
@@ -138,7 +145,6 @@ var token = (key, options) => {
|
|
|
138
145
|
// Annotate the CommonJS export names for ESM import in node:
|
|
139
146
|
0 && (module.exports = {
|
|
140
147
|
InvalidI18nMessageTokenError,
|
|
141
|
-
canonicalizeTokenOptions,
|
|
142
148
|
token
|
|
143
149
|
});
|
|
144
150
|
//# sourceMappingURL=Token.cjs.map
|
package/dist/Token.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Token.ts","../src/internal/token/token.ts"],"sourcesContent":["export type {\n I18nMessageToken,\n
|
|
1
|
+
{"version":3,"sources":["../src/Token.ts","../src/internal/token/token.ts"],"sourcesContent":["export type {\n I18nMessageToken,\n I18nTokenParams,\n I18nTokenParamsInput,\n InvalidI18nMessageTokenReason,\n JsonPrimitive,\n} from './internal/token/token.js'\nexport { InvalidI18nMessageTokenError, token } from './internal/token/token.js'\n","export type JsonPrimitive = null | boolean | number | string\n\nexport type I18nTokenParams = Readonly<Record<string, JsonPrimitive>>\nexport type I18nTokenParamsInput = Readonly<Record<string, JsonPrimitive | undefined>>\n\nexport type I18nMessageToken = {\n readonly _tag: 'i18n'\n readonly key: string\n readonly params?: I18nTokenParams\n}\n\nexport type InvalidI18nMessageTokenReason =\n | 'keyTooLong'\n | 'tooManyParams'\n | 'paramKeyInvalid'\n | 'paramValueInvalid'\n | 'paramValueTooLong'\n | 'numberNotJsonSafe'\n | 'languageFrozen'\n | 'renderFallbackReserved'\n\nexport class InvalidI18nMessageTokenError extends Error {\n readonly name = 'InvalidI18nMessageTokenError'\n\n constructor(\n readonly reason: InvalidI18nMessageTokenReason,\n readonly details: unknown,\n readonly fix: ReadonlyArray<string>,\n ) {\n super(`[InvalidI18nMessageTokenError] reason=${reason}`)\n }\n}\n\nconst TOKEN_BUDGET = {\n keyMaxLen: 96,\n paramKeyMaxCount: 8,\n paramValueStringMaxLen: 96,\n} as const\n\nconst LANGUAGE_FROZEN_KEYS = new Set(['lng', 'lngs'])\nconst DANGEROUS_PARAM_KEYS = new Set(['__proto__', 'prototype', 'constructor'])\nconst LEGACY_RENDER_FALLBACK_KEY = `default${'Value'}`\nconst RESERVED_RENDER_FALLBACK_KEYS = new Set([LEGACY_RENDER_FALLBACK_KEY])\n\nconst isJsonPrimitive = (value: unknown): value is JsonPrimitive =>\n value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string'\n\nconst invalidToken = (reason: InvalidI18nMessageTokenReason, details: unknown, fix: ReadonlyArray<string>): never => {\n throw new InvalidI18nMessageTokenError(reason, details, fix)\n}\n\nexport const canonicalizeTokenParams = (params: I18nTokenParamsInput | undefined): I18nTokenParams | undefined => {\n if (!params) return undefined\n if (typeof params !== 'object' || params === null || Array.isArray(params)) {\n invalidToken('paramValueInvalid', { field: 'params', actual: typeof params }, [\n '请传入 plain object 作为 params(Record<string, JsonPrimitive>)。',\n '不要传入数组/函数/类实例等不可序列化值。',\n ])\n }\n\n const entries = Object.entries(params).filter((p): p is [string, JsonPrimitive] => p[1] !== undefined)\n\n if (entries.length === 0) return undefined\n\n if (entries.length > TOKEN_BUDGET.paramKeyMaxCount) {\n invalidToken(\n 'tooManyParams',\n {\n field: 'params',\n max: TOKEN_BUDGET.paramKeyMaxCount,\n actual: entries.length,\n },\n [\n `减少 params 键数量(建议 ≤ ${TOKEN_BUDGET.paramKeyMaxCount})。`,\n '避免把大对象或展示兜底文案塞进 semantic token。',\n ],\n )\n }\n\n for (const [k, v] of entries) {\n if (RESERVED_RENDER_FALLBACK_KEYS.has(k)) {\n invalidToken('renderFallbackReserved', { field: `params.${k}`, key: k }, [\n '不要把旧的展示兜底字段放进 semantic token params。',\n '请把兜底文案改放到 render/renderReady 的 hints.fallback。',\n ])\n }\n\n if (DANGEROUS_PARAM_KEYS.has(k) || k.length === 0) {\n invalidToken('paramKeyInvalid', { field: `params.${k}`, key: k }, [\n '请使用普通字段名作为 params key,避免 __proto__/constructor/prototype 等危险键。',\n '如需传递复杂结构,请先在展示边界转换为字符串。',\n ])\n }\n\n if (LANGUAGE_FROZEN_KEYS.has(k)) {\n invalidToken('languageFrozen', { field: `params.${k}`, key: k }, [\n '不要在 token params 中传入 lng/lngs 等语言冻结字段。',\n '语言由外部 i18n 实例决定;token 只表达“要翻译什么”。',\n ])\n }\n\n if (!isJsonPrimitive(v)) {\n invalidToken('paramValueInvalid', { field: `params.${k}`, actual: typeof v }, [\n 'params value 只允许 JsonPrimitive(null/boolean/number/string)。',\n '不要传入对象/数组/函数;需要时请在展示边界先格式化成字符串。',\n ])\n }\n\n if (typeof v === 'number' && !Number.isFinite(v)) {\n invalidToken('numberNotJsonSafe', { field: `params.${k}`, value: String(v) }, [\n '不要在 token params 中传入 NaN/Infinity。',\n '请先把该数值转换为可 JSON 化的 number 或 string。',\n ])\n }\n\n if (typeof v === 'string' && v.length > TOKEN_BUDGET.paramValueStringMaxLen) {\n invalidToken(\n 'paramValueTooLong',\n {\n field: `params.${k}`,\n maxLen: TOKEN_BUDGET.paramValueStringMaxLen,\n actualLen: v.length,\n },\n [\n `缩短字符串值长度(建议 ≤ ${TOKEN_BUDGET.paramValueStringMaxLen})。`,\n '把长文本留在展示边界,不要放进 semantic token。',\n ],\n )\n }\n }\n\n entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n\n const out: Record<string, JsonPrimitive> = {}\n for (const [k, v] of entries) {\n out[k] = v\n }\n return out\n}\n\nexport const token = (key: string, params?: I18nTokenParamsInput): I18nMessageToken => {\n if (key.length > TOKEN_BUDGET.keyMaxLen) {\n invalidToken('keyTooLong', { field: 'key', maxLen: TOKEN_BUDGET.keyMaxLen, actualLen: key.length }, [\n `缩短 key(建议 ≤ ${TOKEN_BUDGET.keyMaxLen})。`,\n '如果 key 过长,建议改为“稳定 key + 少量 semantic params”。',\n ])\n }\n\n const canon = canonicalizeTokenParams(params)\n return canon\n ? {\n _tag: 'i18n',\n key,\n params: canon,\n }\n : {\n _tag: 'i18n',\n key,\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/Token.d.cts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
type JsonPrimitive = null | boolean | number | string;
|
|
2
|
-
type
|
|
3
|
-
type
|
|
2
|
+
type I18nTokenParams = Readonly<Record<string, JsonPrimitive>>;
|
|
3
|
+
type I18nTokenParamsInput = Readonly<Record<string, JsonPrimitive | undefined>>;
|
|
4
4
|
type I18nMessageToken = {
|
|
5
5
|
readonly _tag: 'i18n';
|
|
6
6
|
readonly key: string;
|
|
7
|
-
readonly
|
|
7
|
+
readonly params?: I18nTokenParams;
|
|
8
8
|
};
|
|
9
|
-
type InvalidI18nMessageTokenReason = 'keyTooLong' | '
|
|
9
|
+
type InvalidI18nMessageTokenReason = 'keyTooLong' | 'tooManyParams' | 'paramKeyInvalid' | 'paramValueInvalid' | 'paramValueTooLong' | 'numberNotJsonSafe' | 'languageFrozen' | 'renderFallbackReserved';
|
|
10
10
|
declare class InvalidI18nMessageTokenError extends Error {
|
|
11
11
|
readonly reason: InvalidI18nMessageTokenReason;
|
|
12
12
|
readonly details: unknown;
|
|
@@ -14,7 +14,6 @@ declare class InvalidI18nMessageTokenError extends Error {
|
|
|
14
14
|
readonly name = "InvalidI18nMessageTokenError";
|
|
15
15
|
constructor(reason: InvalidI18nMessageTokenReason, details: unknown, fix: ReadonlyArray<string>);
|
|
16
16
|
}
|
|
17
|
-
declare const
|
|
18
|
-
declare const token: (key: string, options?: I18nTokenOptionsInput) => I18nMessageToken;
|
|
17
|
+
declare const token: (key: string, params?: I18nTokenParamsInput) => I18nMessageToken;
|
|
19
18
|
|
|
20
|
-
export { type I18nMessageToken, type
|
|
19
|
+
export { type I18nMessageToken, type I18nTokenParams, type I18nTokenParamsInput, InvalidI18nMessageTokenError, type InvalidI18nMessageTokenReason, type JsonPrimitive, token };
|
package/dist/Token.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
type JsonPrimitive = null | boolean | number | string;
|
|
2
|
-
type
|
|
3
|
-
type
|
|
2
|
+
type I18nTokenParams = Readonly<Record<string, JsonPrimitive>>;
|
|
3
|
+
type I18nTokenParamsInput = Readonly<Record<string, JsonPrimitive | undefined>>;
|
|
4
4
|
type I18nMessageToken = {
|
|
5
5
|
readonly _tag: 'i18n';
|
|
6
6
|
readonly key: string;
|
|
7
|
-
readonly
|
|
7
|
+
readonly params?: I18nTokenParams;
|
|
8
8
|
};
|
|
9
|
-
type InvalidI18nMessageTokenReason = 'keyTooLong' | '
|
|
9
|
+
type InvalidI18nMessageTokenReason = 'keyTooLong' | 'tooManyParams' | 'paramKeyInvalid' | 'paramValueInvalid' | 'paramValueTooLong' | 'numberNotJsonSafe' | 'languageFrozen' | 'renderFallbackReserved';
|
|
10
10
|
declare class InvalidI18nMessageTokenError extends Error {
|
|
11
11
|
readonly reason: InvalidI18nMessageTokenReason;
|
|
12
12
|
readonly details: unknown;
|
|
@@ -14,7 +14,6 @@ declare class InvalidI18nMessageTokenError extends Error {
|
|
|
14
14
|
readonly name = "InvalidI18nMessageTokenError";
|
|
15
15
|
constructor(reason: InvalidI18nMessageTokenReason, details: unknown, fix: ReadonlyArray<string>);
|
|
16
16
|
}
|
|
17
|
-
declare const
|
|
18
|
-
declare const token: (key: string, options?: I18nTokenOptionsInput) => I18nMessageToken;
|
|
17
|
+
declare const token: (key: string, params?: I18nTokenParamsInput) => I18nMessageToken;
|
|
19
18
|
|
|
20
|
-
export { type I18nMessageToken, type
|
|
19
|
+
export { type I18nMessageToken, type I18nTokenParams, type I18nTokenParamsInput, InvalidI18nMessageTokenError, type InvalidI18nMessageTokenReason, type JsonPrimitive, token };
|
package/dist/Token.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import "./chunk-LW6LHDDL.js";
|
|
2
1
|
import {
|
|
3
2
|
InvalidI18nMessageTokenError,
|
|
4
|
-
canonicalizeTokenOptions,
|
|
5
3
|
token
|
|
6
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LGNB43KG.js";
|
|
7
5
|
export {
|
|
8
6
|
InvalidI18nMessageTokenError,
|
|
9
|
-
canonicalizeTokenOptions,
|
|
10
7
|
token
|
|
11
8
|
};
|
|
12
9
|
//# sourceMappingURL=Token.js.map
|