@pactor-app/i18n 0.0.0-stage → 1.54.5
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/LICENSE +21 -0
- package/README.md +118 -2
- package/dist/chunk-3YCHXWF6.js +1032 -0
- package/dist/index.cjs +1075 -0
- package/dist/index.d.cts +258 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +40 -0
- package/dist/node.cjs +1354 -0
- package/dist/node.d.cts +99 -0
- package/dist/node.d.ts +99 -0
- package/dist/node.js +315 -0
- package/package.json +75 -3
package/dist/node.d.cts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { MessageProvider, LoadMessagesInput, TranslatableFieldRegistry, AuthorPageDsl, LocalizeDiagnostic } from './index.cjs';
|
|
2
|
+
export { AcceptLanguageEntry, ApiLoadInput, ApiLoadResult, ApiMessageProvider, ApiMessageProviderOptions, AuthorPageI18n, EnvelopeExpectation, FetchLike, FetchLikeResponse, I18nError, I18nErrorCode, LanguageBundles, LocaleResolverConfig, LocaleSource, LocalizePageDslInput, MISSING_TRANSLATION_TEXT, MissingResolution, ResolveLocaleInput, ResolvedJsonPointer, ResolvedRequestLocale, TranslationDescriptor, assertMessages, assertNoCrossNamespaceKeyConflicts, createDefaultTranslatableFieldRegistry, escapeJsonPointerSegment, getDefaultTranslatableFieldRegistry, isI18nError, isValidMessageKey, localizePageDsl, normalizeLocaleTag, parseAcceptLanguage, parseJsonPointer, resolveJsonPointer, resolveRequestLocale, validateBundleSetEnvelope } from './index.cjs';
|
|
3
|
+
import { MessageBundleSet, PageDsl, LocalizationMeta } from '@pactor-app/core';
|
|
4
|
+
|
|
5
|
+
/** manifest 中单个 namespace 的条目:文件相对路径与内容版本 */
|
|
6
|
+
interface MessageManifestEntry {
|
|
7
|
+
path: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 语言包发布清单(设计 6.0.2):记录应用、release、语言、namespace、
|
|
12
|
+
* 文件相对路径和内容版本。文件路径只从该受信任清单取得。
|
|
13
|
+
*/
|
|
14
|
+
interface MessageManifest {
|
|
15
|
+
appId: string;
|
|
16
|
+
release: string;
|
|
17
|
+
/** locale → namespace → 条目 */
|
|
18
|
+
locales: Record<string, Record<string, MessageManifestEntry>>;
|
|
19
|
+
}
|
|
20
|
+
interface YamlMessageProviderOptions {
|
|
21
|
+
/** 清单普通对象(构造时校验结构) */
|
|
22
|
+
manifest: unknown;
|
|
23
|
+
/** 注入的文件读取函数(Node 适配由应用端提供),参数为清单中的相对路径 */
|
|
24
|
+
readFile: (path: string) => Promise<string>;
|
|
25
|
+
/** 单文件大小上限(字节),默认 1MB */
|
|
26
|
+
maxFileBytes?: number;
|
|
27
|
+
/** YAML 别名展开上限,默认 100 */
|
|
28
|
+
maxAliasCount?: number;
|
|
29
|
+
}
|
|
30
|
+
/** 校验 manifest 结构;非法抛 MANIFEST_INVALID */
|
|
31
|
+
declare function validateManifest(manifest: unknown): MessageManifest;
|
|
32
|
+
/**
|
|
33
|
+
* 本地 YAML 语言包 Provider(设计 6.0.2),仅用于 Node 入口
|
|
34
|
+
* (`@pactor-app/i18n/node`,依赖 peerDependency `yaml`)。
|
|
35
|
+
*
|
|
36
|
+
* 按 manifest 定位 locale/namespace 文件并严格解析:语法错误、重复 key、
|
|
37
|
+
* 自定义标签、非字符串词条、超出大小/别名展开限制均拒绝;加载后检查
|
|
38
|
+
* namespace 间完整 key 冲突;输出与 API 来源相同的 MessageBundleSet。
|
|
39
|
+
*/
|
|
40
|
+
declare class YamlMessageProvider implements MessageProvider {
|
|
41
|
+
private readonly manifest;
|
|
42
|
+
private readonly readFile;
|
|
43
|
+
private readonly maxFileBytes;
|
|
44
|
+
private readonly maxAliasCount;
|
|
45
|
+
constructor(options: YamlMessageProviderOptions);
|
|
46
|
+
load(input: LoadMessagesInput): Promise<MessageBundleSet>;
|
|
47
|
+
/** 读取并严格解析单个语言包 YAML 文件 */
|
|
48
|
+
private loadBundle;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* YAML 资源转换编排入口(设计 6.0.2,缺口 G18),仅 Node 入口导出
|
|
53
|
+
* (页面 YAML 严格解析依赖 peerDependency `yaml`)。
|
|
54
|
+
*
|
|
55
|
+
* 流程:加载文本 → 严格解析 → 作者态校验(@pactor-app/schema)→
|
|
56
|
+
* 可选裁剪钩子 → 提取 namespaces → Provider 批量加载当前语言与回退语言包 →
|
|
57
|
+
* localizePageDsl → 下发态校验 → 返回 `{ schema, localization }`。
|
|
58
|
+
* 一次请求内语言包经一次 Promise.all 批量获取,不在遍历组件时逐个请求。
|
|
59
|
+
*/
|
|
60
|
+
interface ParsePageYamlOptions {
|
|
61
|
+
/** YAML 别名展开上限,默认 100 */
|
|
62
|
+
maxAliasCount?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 页面 YAML 严格解析:语法错误、重复 key、自定义标签、超出别名展开限制
|
|
66
|
+
* 均抛 PAGE_YAML_INVALID。可作为 localizeYamlResource 的 parseYaml 直接使用。
|
|
67
|
+
*/
|
|
68
|
+
declare function parsePageYamlStrict(text: string, options?: ParsePageYamlOptions): unknown;
|
|
69
|
+
interface LocalizeYamlResourceInput {
|
|
70
|
+
/** 加载页面 YAML 原文(本地文件或后端接口,由调用方注入) */
|
|
71
|
+
loadText: () => Promise<string>;
|
|
72
|
+
/** 注入的严格 YAML 解析;缺省为 parsePageYamlStrict */
|
|
73
|
+
parseYaml?: (text: string) => unknown;
|
|
74
|
+
/** 语言包 Provider(YamlMessageProvider / ApiMessageProvider 均可) */
|
|
75
|
+
provider: MessageProvider;
|
|
76
|
+
appId: string;
|
|
77
|
+
/** 来自服务端可信上下文 */
|
|
78
|
+
tenantId?: string;
|
|
79
|
+
release: string;
|
|
80
|
+
locale: string;
|
|
81
|
+
defaultLocale: string;
|
|
82
|
+
/** 显式语言级回退(有序,如 fr-CA → ['fr']),位于当前语言与 defaultLocale 之间 */
|
|
83
|
+
fallbackLocales?: string[];
|
|
84
|
+
registry?: TranslatableFieldRegistry;
|
|
85
|
+
/** 可选会话裁剪钩子:在作者态校验后、提取 namespaces 前执行 */
|
|
86
|
+
prune?: (page: AuthorPageDsl) => AuthorPageDsl;
|
|
87
|
+
/** localization 元信息透传(来自 resolveRequestLocale 结果) */
|
|
88
|
+
source?: string;
|
|
89
|
+
explicit?: boolean;
|
|
90
|
+
onDiagnostic?: (diagnostic: LocalizeDiagnostic) => void;
|
|
91
|
+
}
|
|
92
|
+
interface LocalizeYamlResourceResult {
|
|
93
|
+
/** 已本地化并通过下发态校验的 Page DSL */
|
|
94
|
+
schema: PageDsl;
|
|
95
|
+
localization: LocalizationMeta;
|
|
96
|
+
}
|
|
97
|
+
declare function localizeYamlResource(input: LocalizeYamlResourceInput): Promise<LocalizeYamlResourceResult>;
|
|
98
|
+
|
|
99
|
+
export { AuthorPageDsl, LoadMessagesInput, LocalizeDiagnostic, type LocalizeYamlResourceInput, type LocalizeYamlResourceResult, type MessageManifest, type MessageManifestEntry, MessageProvider, type ParsePageYamlOptions, TranslatableFieldRegistry, YamlMessageProvider, type YamlMessageProviderOptions, localizeYamlResource, parsePageYamlStrict, validateManifest };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { MessageProvider, LoadMessagesInput, TranslatableFieldRegistry, AuthorPageDsl, LocalizeDiagnostic } from './index.js';
|
|
2
|
+
export { AcceptLanguageEntry, ApiLoadInput, ApiLoadResult, ApiMessageProvider, ApiMessageProviderOptions, AuthorPageI18n, EnvelopeExpectation, FetchLike, FetchLikeResponse, I18nError, I18nErrorCode, LanguageBundles, LocaleResolverConfig, LocaleSource, LocalizePageDslInput, MISSING_TRANSLATION_TEXT, MissingResolution, ResolveLocaleInput, ResolvedJsonPointer, ResolvedRequestLocale, TranslationDescriptor, assertMessages, assertNoCrossNamespaceKeyConflicts, createDefaultTranslatableFieldRegistry, escapeJsonPointerSegment, getDefaultTranslatableFieldRegistry, isI18nError, isValidMessageKey, localizePageDsl, normalizeLocaleTag, parseAcceptLanguage, parseJsonPointer, resolveJsonPointer, resolveRequestLocale, validateBundleSetEnvelope } from './index.js';
|
|
3
|
+
import { MessageBundleSet, PageDsl, LocalizationMeta } from '@pactor-app/core';
|
|
4
|
+
|
|
5
|
+
/** manifest 中单个 namespace 的条目:文件相对路径与内容版本 */
|
|
6
|
+
interface MessageManifestEntry {
|
|
7
|
+
path: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 语言包发布清单(设计 6.0.2):记录应用、release、语言、namespace、
|
|
12
|
+
* 文件相对路径和内容版本。文件路径只从该受信任清单取得。
|
|
13
|
+
*/
|
|
14
|
+
interface MessageManifest {
|
|
15
|
+
appId: string;
|
|
16
|
+
release: string;
|
|
17
|
+
/** locale → namespace → 条目 */
|
|
18
|
+
locales: Record<string, Record<string, MessageManifestEntry>>;
|
|
19
|
+
}
|
|
20
|
+
interface YamlMessageProviderOptions {
|
|
21
|
+
/** 清单普通对象(构造时校验结构) */
|
|
22
|
+
manifest: unknown;
|
|
23
|
+
/** 注入的文件读取函数(Node 适配由应用端提供),参数为清单中的相对路径 */
|
|
24
|
+
readFile: (path: string) => Promise<string>;
|
|
25
|
+
/** 单文件大小上限(字节),默认 1MB */
|
|
26
|
+
maxFileBytes?: number;
|
|
27
|
+
/** YAML 别名展开上限,默认 100 */
|
|
28
|
+
maxAliasCount?: number;
|
|
29
|
+
}
|
|
30
|
+
/** 校验 manifest 结构;非法抛 MANIFEST_INVALID */
|
|
31
|
+
declare function validateManifest(manifest: unknown): MessageManifest;
|
|
32
|
+
/**
|
|
33
|
+
* 本地 YAML 语言包 Provider(设计 6.0.2),仅用于 Node 入口
|
|
34
|
+
* (`@pactor-app/i18n/node`,依赖 peerDependency `yaml`)。
|
|
35
|
+
*
|
|
36
|
+
* 按 manifest 定位 locale/namespace 文件并严格解析:语法错误、重复 key、
|
|
37
|
+
* 自定义标签、非字符串词条、超出大小/别名展开限制均拒绝;加载后检查
|
|
38
|
+
* namespace 间完整 key 冲突;输出与 API 来源相同的 MessageBundleSet。
|
|
39
|
+
*/
|
|
40
|
+
declare class YamlMessageProvider implements MessageProvider {
|
|
41
|
+
private readonly manifest;
|
|
42
|
+
private readonly readFile;
|
|
43
|
+
private readonly maxFileBytes;
|
|
44
|
+
private readonly maxAliasCount;
|
|
45
|
+
constructor(options: YamlMessageProviderOptions);
|
|
46
|
+
load(input: LoadMessagesInput): Promise<MessageBundleSet>;
|
|
47
|
+
/** 读取并严格解析单个语言包 YAML 文件 */
|
|
48
|
+
private loadBundle;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* YAML 资源转换编排入口(设计 6.0.2,缺口 G18),仅 Node 入口导出
|
|
53
|
+
* (页面 YAML 严格解析依赖 peerDependency `yaml`)。
|
|
54
|
+
*
|
|
55
|
+
* 流程:加载文本 → 严格解析 → 作者态校验(@pactor-app/schema)→
|
|
56
|
+
* 可选裁剪钩子 → 提取 namespaces → Provider 批量加载当前语言与回退语言包 →
|
|
57
|
+
* localizePageDsl → 下发态校验 → 返回 `{ schema, localization }`。
|
|
58
|
+
* 一次请求内语言包经一次 Promise.all 批量获取,不在遍历组件时逐个请求。
|
|
59
|
+
*/
|
|
60
|
+
interface ParsePageYamlOptions {
|
|
61
|
+
/** YAML 别名展开上限,默认 100 */
|
|
62
|
+
maxAliasCount?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 页面 YAML 严格解析:语法错误、重复 key、自定义标签、超出别名展开限制
|
|
66
|
+
* 均抛 PAGE_YAML_INVALID。可作为 localizeYamlResource 的 parseYaml 直接使用。
|
|
67
|
+
*/
|
|
68
|
+
declare function parsePageYamlStrict(text: string, options?: ParsePageYamlOptions): unknown;
|
|
69
|
+
interface LocalizeYamlResourceInput {
|
|
70
|
+
/** 加载页面 YAML 原文(本地文件或后端接口,由调用方注入) */
|
|
71
|
+
loadText: () => Promise<string>;
|
|
72
|
+
/** 注入的严格 YAML 解析;缺省为 parsePageYamlStrict */
|
|
73
|
+
parseYaml?: (text: string) => unknown;
|
|
74
|
+
/** 语言包 Provider(YamlMessageProvider / ApiMessageProvider 均可) */
|
|
75
|
+
provider: MessageProvider;
|
|
76
|
+
appId: string;
|
|
77
|
+
/** 来自服务端可信上下文 */
|
|
78
|
+
tenantId?: string;
|
|
79
|
+
release: string;
|
|
80
|
+
locale: string;
|
|
81
|
+
defaultLocale: string;
|
|
82
|
+
/** 显式语言级回退(有序,如 fr-CA → ['fr']),位于当前语言与 defaultLocale 之间 */
|
|
83
|
+
fallbackLocales?: string[];
|
|
84
|
+
registry?: TranslatableFieldRegistry;
|
|
85
|
+
/** 可选会话裁剪钩子:在作者态校验后、提取 namespaces 前执行 */
|
|
86
|
+
prune?: (page: AuthorPageDsl) => AuthorPageDsl;
|
|
87
|
+
/** localization 元信息透传(来自 resolveRequestLocale 结果) */
|
|
88
|
+
source?: string;
|
|
89
|
+
explicit?: boolean;
|
|
90
|
+
onDiagnostic?: (diagnostic: LocalizeDiagnostic) => void;
|
|
91
|
+
}
|
|
92
|
+
interface LocalizeYamlResourceResult {
|
|
93
|
+
/** 已本地化并通过下发态校验的 Page DSL */
|
|
94
|
+
schema: PageDsl;
|
|
95
|
+
localization: LocalizationMeta;
|
|
96
|
+
}
|
|
97
|
+
declare function localizeYamlResource(input: LocalizeYamlResourceInput): Promise<LocalizeYamlResourceResult>;
|
|
98
|
+
|
|
99
|
+
export { AuthorPageDsl, LoadMessagesInput, LocalizeDiagnostic, type LocalizeYamlResourceInput, type LocalizeYamlResourceResult, type MessageManifest, type MessageManifestEntry, MessageProvider, type ParsePageYamlOptions, TranslatableFieldRegistry, YamlMessageProvider, type YamlMessageProviderOptions, localizeYamlResource, parsePageYamlStrict, validateManifest };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiMessageProvider,
|
|
3
|
+
I18nError,
|
|
4
|
+
MISSING_TRANSLATION_TEXT,
|
|
5
|
+
TranslatableFieldRegistry,
|
|
6
|
+
assertMessages,
|
|
7
|
+
assertNoCrossNamespaceKeyConflicts,
|
|
8
|
+
createDefaultTranslatableFieldRegistry,
|
|
9
|
+
escapeJsonPointerSegment,
|
|
10
|
+
getDefaultTranslatableFieldRegistry,
|
|
11
|
+
isI18nError,
|
|
12
|
+
isValidMessageKey,
|
|
13
|
+
localizePageDsl,
|
|
14
|
+
normalizeLocaleTag,
|
|
15
|
+
parseAcceptLanguage,
|
|
16
|
+
parseJsonPointer,
|
|
17
|
+
resolveJsonPointer,
|
|
18
|
+
resolveRequestLocale,
|
|
19
|
+
validateBundleSetEnvelope
|
|
20
|
+
} from "./chunk-3YCHXWF6.js";
|
|
21
|
+
|
|
22
|
+
// src/yaml-message-provider.ts
|
|
23
|
+
import { isMap, isPair, isScalar, isSeq, parseDocument } from "yaml";
|
|
24
|
+
var DEFAULT_MAX_FILE_BYTES = 1e6;
|
|
25
|
+
var DEFAULT_MAX_ALIAS_COUNT = 100;
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function isSafeRelativePath(path) {
|
|
30
|
+
const normalized = path.replace(/\\/g, "/");
|
|
31
|
+
return normalized !== "" && !normalized.startsWith("/") && !/^[A-Za-z]:\//.test(normalized) && !normalized.split("/").includes("..");
|
|
32
|
+
}
|
|
33
|
+
function validateManifest(manifest) {
|
|
34
|
+
if (!isPlainObject(manifest)) {
|
|
35
|
+
throw new I18nError("MANIFEST_INVALID", "\u8BED\u8A00\u5305\u6E05\u5355\u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
36
|
+
}
|
|
37
|
+
if (typeof manifest["appId"] !== "string" || manifest["appId"] === "") {
|
|
38
|
+
throw new I18nError("MANIFEST_INVALID", "\u6E05\u5355\u7F3A\u5C11 appId");
|
|
39
|
+
}
|
|
40
|
+
if (typeof manifest["release"] !== "string" || manifest["release"] === "") {
|
|
41
|
+
throw new I18nError("MANIFEST_INVALID", "\u6E05\u5355\u7F3A\u5C11 release");
|
|
42
|
+
}
|
|
43
|
+
const locales = manifest["locales"];
|
|
44
|
+
if (!isPlainObject(locales)) {
|
|
45
|
+
throw new I18nError("MANIFEST_INVALID", "\u6E05\u5355\u7F3A\u5C11 locales");
|
|
46
|
+
}
|
|
47
|
+
for (const [locale, namespaces] of Object.entries(locales)) {
|
|
48
|
+
if (!isPlainObject(namespaces)) {
|
|
49
|
+
throw new I18nError("MANIFEST_INVALID", `\u6E05\u5355 locales.${locale} \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
50
|
+
}
|
|
51
|
+
for (const [namespace, entry] of Object.entries(namespaces)) {
|
|
52
|
+
if (!isPlainObject(entry) || typeof entry["path"] !== "string" || typeof entry["version"] !== "string") {
|
|
53
|
+
throw new I18nError("MANIFEST_INVALID", `\u6E05\u5355\u6761\u76EE\u7F3A\u5C11 path/version: ${locale}/${namespace}`);
|
|
54
|
+
}
|
|
55
|
+
if (!isSafeRelativePath(entry["path"])) {
|
|
56
|
+
throw new I18nError("MANIFEST_INVALID", `\u6E05\u5355\u6761\u76EE\u8DEF\u5F84\u5FC5\u987B\u662F\u5B89\u5168\u76F8\u5BF9\u8DEF\u5F84: ${entry["path"]}`, {
|
|
57
|
+
locale,
|
|
58
|
+
namespace
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return manifest;
|
|
64
|
+
}
|
|
65
|
+
function byteLength(text) {
|
|
66
|
+
let bytes = 0;
|
|
67
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
68
|
+
const code = text.charCodeAt(i);
|
|
69
|
+
if (code < 128) {
|
|
70
|
+
bytes += 1;
|
|
71
|
+
} else if (code < 2048) {
|
|
72
|
+
bytes += 2;
|
|
73
|
+
} else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
|
|
74
|
+
bytes += 4;
|
|
75
|
+
i += 1;
|
|
76
|
+
} else {
|
|
77
|
+
bytes += 3;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return bytes;
|
|
81
|
+
}
|
|
82
|
+
function hasExplicitTag(node) {
|
|
83
|
+
if (isPair(node)) {
|
|
84
|
+
return hasExplicitTag(node.key) || hasExplicitTag(node.value);
|
|
85
|
+
}
|
|
86
|
+
if (isMap(node) || isSeq(node)) {
|
|
87
|
+
return node.tag !== void 0 || node.items.some(hasExplicitTag);
|
|
88
|
+
}
|
|
89
|
+
if (isScalar(node)) {
|
|
90
|
+
return node.tag !== void 0;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
var YamlMessageProvider = class {
|
|
95
|
+
constructor(options) {
|
|
96
|
+
this.manifest = validateManifest(options.manifest);
|
|
97
|
+
this.readFile = options.readFile;
|
|
98
|
+
this.maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
99
|
+
this.maxAliasCount = options.maxAliasCount ?? DEFAULT_MAX_ALIAS_COUNT;
|
|
100
|
+
}
|
|
101
|
+
async load(input) {
|
|
102
|
+
if (input.appId !== this.manifest.appId || input.release !== this.manifest.release) {
|
|
103
|
+
throw new I18nError("MANIFEST_MISMATCH", "\u8BF7\u6C42 appId/release \u4E0E\u8BED\u8A00\u5305\u6E05\u5355\u4E0D\u4E00\u81F4", {
|
|
104
|
+
appId: input.appId,
|
|
105
|
+
release: input.release,
|
|
106
|
+
manifestAppId: this.manifest.appId,
|
|
107
|
+
manifestRelease: this.manifest.release
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const localeEntries = this.manifest.locales[input.locale];
|
|
111
|
+
if (localeEntries === void 0) {
|
|
112
|
+
throw new I18nError("NAMESPACE_NOT_FOUND", `\u6E05\u5355\u4E0D\u542B\u8BED\u8A00: ${input.locale}`, {
|
|
113
|
+
locale: input.locale
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const bundles = {};
|
|
117
|
+
for (const namespace of input.namespaces) {
|
|
118
|
+
const entry = localeEntries[namespace];
|
|
119
|
+
if (entry === void 0) {
|
|
120
|
+
throw new I18nError(
|
|
121
|
+
"NAMESPACE_NOT_FOUND",
|
|
122
|
+
`\u6E05\u5355\u4E0D\u542B namespace: ${input.locale}/${namespace}`,
|
|
123
|
+
{ locale: input.locale, namespace }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
bundles[namespace] = {
|
|
127
|
+
version: entry.version,
|
|
128
|
+
messages: await this.loadBundle(entry.path, namespace)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
assertNoCrossNamespaceKeyConflicts(
|
|
132
|
+
Object.fromEntries(Object.entries(bundles).map(([ns, b]) => [ns, b.messages])),
|
|
133
|
+
"MESSAGE_BUNDLE_INVALID"
|
|
134
|
+
);
|
|
135
|
+
return {
|
|
136
|
+
appId: this.manifest.appId,
|
|
137
|
+
locale: input.locale,
|
|
138
|
+
release: this.manifest.release,
|
|
139
|
+
format: "simple-v1",
|
|
140
|
+
bundles
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** 读取并严格解析单个语言包 YAML 文件 */
|
|
144
|
+
async loadBundle(path, namespace) {
|
|
145
|
+
let text;
|
|
146
|
+
try {
|
|
147
|
+
text = await this.readFile(path);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new I18nError("MESSAGE_FETCH_FAILED", `\u8BED\u8A00\u5305\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25: ${path}`, {
|
|
150
|
+
path,
|
|
151
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const context = `\u8BED\u8A00\u5305 ${namespace}\uFF08${path}\uFF09`;
|
|
155
|
+
if (byteLength(text) > this.maxFileBytes) {
|
|
156
|
+
throw new I18nError("MESSAGE_BUNDLE_INVALID", `${context}: \u8D85\u51FA\u5927\u5C0F\u9650\u5236 ${this.maxFileBytes} \u5B57\u8282`, {
|
|
157
|
+
path,
|
|
158
|
+
maxFileBytes: this.maxFileBytes
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const doc = parseDocument(text, {
|
|
162
|
+
schema: "core",
|
|
163
|
+
uniqueKeys: true,
|
|
164
|
+
strict: true
|
|
165
|
+
});
|
|
166
|
+
if (doc.errors.length > 0) {
|
|
167
|
+
throw new I18nError(
|
|
168
|
+
"MESSAGE_BUNDLE_INVALID",
|
|
169
|
+
`${context}: YAML \u89E3\u6790\u5931\u8D25: ${doc.errors.map((e) => e.message).join("; ")}`,
|
|
170
|
+
{ path }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (hasExplicitTag(doc.contents)) {
|
|
174
|
+
throw new I18nError("MESSAGE_BUNDLE_INVALID", `${context}: \u4E0D\u5141\u8BB8\u81EA\u5B9A\u4E49\u6807\u7B7E`, { path });
|
|
175
|
+
}
|
|
176
|
+
let data;
|
|
177
|
+
try {
|
|
178
|
+
data = doc.toJS({ maxAliasCount: this.maxAliasCount });
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new I18nError(
|
|
181
|
+
"MESSAGE_BUNDLE_INVALID",
|
|
182
|
+
`${context}: ${error instanceof Error ? error.message : String(error)}`,
|
|
183
|
+
{ path }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return assertMessages(data, context, "MESSAGE_BUNDLE_INVALID");
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/localize-yaml-resource.ts
|
|
191
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
192
|
+
import { validateAuthorPageDsl, validateLocalizedPageDsl } from "@pactor-app/schema";
|
|
193
|
+
function parsePageYamlStrict(text, options) {
|
|
194
|
+
const doc = parseDocument2(text, {
|
|
195
|
+
schema: "core",
|
|
196
|
+
uniqueKeys: true,
|
|
197
|
+
strict: true
|
|
198
|
+
});
|
|
199
|
+
if (doc.errors.length > 0) {
|
|
200
|
+
throw new I18nError(
|
|
201
|
+
"PAGE_YAML_INVALID",
|
|
202
|
+
`\u9875\u9762 YAML \u89E3\u6790\u5931\u8D25: ${doc.errors.map((e) => e.message).join("; ")}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (hasExplicitTag(doc.contents)) {
|
|
206
|
+
throw new I18nError("PAGE_YAML_INVALID", "\u9875\u9762 YAML \u4E0D\u5141\u8BB8\u81EA\u5B9A\u4E49\u6807\u7B7E");
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return doc.toJS({ maxAliasCount: options?.maxAliasCount ?? 100 });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new I18nError(
|
|
212
|
+
"PAGE_YAML_INVALID",
|
|
213
|
+
`\u9875\u9762 YAML \u65E0\u6548: ${error instanceof Error ? error.message : String(error)}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function emptyBundleSet(appId, locale, release) {
|
|
218
|
+
return { appId, locale, release, format: "simple-v1", bundles: {} };
|
|
219
|
+
}
|
|
220
|
+
async function localizeYamlResource(input) {
|
|
221
|
+
let text;
|
|
222
|
+
try {
|
|
223
|
+
text = await input.loadText();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new I18nError("PAGE_LOAD_FAILED", "\u9875\u9762\u8D44\u6E90\u52A0\u8F7D\u5931\u8D25", {
|
|
226
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const parse = input.parseYaml ?? ((value) => parsePageYamlStrict(value));
|
|
230
|
+
let document;
|
|
231
|
+
try {
|
|
232
|
+
document = parse(text);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isI18nError(error)) {
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
throw new I18nError(
|
|
238
|
+
"PAGE_YAML_INVALID",
|
|
239
|
+
`\u9875\u9762 YAML \u89E3\u6790\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const author = validateAuthorPageDsl(document);
|
|
243
|
+
if (!author.valid) {
|
|
244
|
+
throw new I18nError("PAGE_DSL_INVALID", "\u4F5C\u8005\u6001\u9875\u9762\u6821\u9A8C\u5931\u8D25", { errors: author.errors });
|
|
245
|
+
}
|
|
246
|
+
const pruned = input.prune ? input.prune(document) : document;
|
|
247
|
+
const namespaces = pruned.i18n?.namespaces ?? [];
|
|
248
|
+
const chain = [.../* @__PURE__ */ new Set([input.locale, ...input.fallbackLocales ?? [], input.defaultLocale])];
|
|
249
|
+
let current;
|
|
250
|
+
let fallbacks;
|
|
251
|
+
if (namespaces.length === 0) {
|
|
252
|
+
[current, ...fallbacks] = chain.map((loc) => emptyBundleSet(input.appId, loc, input.release));
|
|
253
|
+
} else {
|
|
254
|
+
const loaded = await Promise.all(
|
|
255
|
+
chain.map(
|
|
256
|
+
(loc) => input.provider.load({
|
|
257
|
+
appId: input.appId,
|
|
258
|
+
tenantId: input.tenantId,
|
|
259
|
+
locale: loc,
|
|
260
|
+
namespaces,
|
|
261
|
+
release: input.release
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
[current, ...fallbacks] = loaded;
|
|
266
|
+
}
|
|
267
|
+
const schema = localizePageDsl({
|
|
268
|
+
page: pruned,
|
|
269
|
+
locale: input.locale,
|
|
270
|
+
defaultLocale: input.defaultLocale,
|
|
271
|
+
languageBundles: { current, fallbacks },
|
|
272
|
+
registry: input.registry,
|
|
273
|
+
onDiagnostic: input.onDiagnostic
|
|
274
|
+
});
|
|
275
|
+
const localized = validateLocalizedPageDsl(schema);
|
|
276
|
+
if (!localized.valid) {
|
|
277
|
+
throw new I18nError("PAGE_DSL_INVALID", "\u4E0B\u53D1\u6001\u9875\u9762\u6821\u9A8C\u5931\u8D25", { errors: localized.errors });
|
|
278
|
+
}
|
|
279
|
+
const bundleVersions = Object.fromEntries(
|
|
280
|
+
Object.entries(current.bundles).map(([namespace, bundle]) => [namespace, bundle.version])
|
|
281
|
+
);
|
|
282
|
+
const localization = {
|
|
283
|
+
locale: input.locale,
|
|
284
|
+
defaultLocale: input.defaultLocale,
|
|
285
|
+
release: input.release,
|
|
286
|
+
bundleVersions,
|
|
287
|
+
...input.source !== void 0 ? { source: input.source } : {},
|
|
288
|
+
...input.explicit !== void 0 ? { explicit: input.explicit } : {}
|
|
289
|
+
};
|
|
290
|
+
return { schema, localization };
|
|
291
|
+
}
|
|
292
|
+
export {
|
|
293
|
+
ApiMessageProvider,
|
|
294
|
+
I18nError,
|
|
295
|
+
MISSING_TRANSLATION_TEXT,
|
|
296
|
+
TranslatableFieldRegistry,
|
|
297
|
+
YamlMessageProvider,
|
|
298
|
+
assertMessages,
|
|
299
|
+
assertNoCrossNamespaceKeyConflicts,
|
|
300
|
+
createDefaultTranslatableFieldRegistry,
|
|
301
|
+
escapeJsonPointerSegment,
|
|
302
|
+
getDefaultTranslatableFieldRegistry,
|
|
303
|
+
isI18nError,
|
|
304
|
+
isValidMessageKey,
|
|
305
|
+
localizePageDsl,
|
|
306
|
+
localizeYamlResource,
|
|
307
|
+
normalizeLocaleTag,
|
|
308
|
+
parseAcceptLanguage,
|
|
309
|
+
parseJsonPointer,
|
|
310
|
+
parsePageYamlStrict,
|
|
311
|
+
resolveJsonPointer,
|
|
312
|
+
resolveRequestLocale,
|
|
313
|
+
validateBundleSetEnvelope,
|
|
314
|
+
validateManifest
|
|
315
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pactor-app/i18n",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.54.5",
|
|
4
|
+
"description": "Pactor i18n: request locale resolution and message bundle providers (YAML / API)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pactor",
|
|
7
|
+
"dsl",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"i18n",
|
|
10
|
+
"localization",
|
|
11
|
+
"locale",
|
|
12
|
+
"messages"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/innospots/pactor.git",
|
|
18
|
+
"directory": "packages/i18n"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/innospots/pactor/tree/main/packages/i18n",
|
|
21
|
+
"bugs": "https://github.com/innospots/pactor/issues",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"default": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"./node": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/node.d.ts",
|
|
40
|
+
"default": "./dist/node.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/node.d.cts",
|
|
44
|
+
"default": "./dist/node.cjs"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"sideEffects": false,
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@pactor-app/core": "1.54.5",
|
|
60
|
+
"@pactor-app/schema": "1.54.5"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"yaml": "^2.9.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"yaml": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"yaml": "^2.9.0"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsup",
|
|
75
|
+
"test": "vitest run",
|
|
76
|
+
"typecheck": "tsc --noEmit"
|
|
77
|
+
}
|
|
6
78
|
}
|