@geekron/strapi 0.2.13 → 0.2.14
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.
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 为翻译对象添加前缀
|
|
3
|
+
* @param translations - 原始翻译对象
|
|
4
|
+
* @param prefix - 前缀字符串
|
|
5
|
+
* @returns 添加前缀后的翻译对象
|
|
6
|
+
*/
|
|
7
|
+
const addPrefix = (translations: Record<string, any>, prefix: string): Record<string, any> => {
|
|
8
|
+
return Object.keys(translations).reduce((acc, key) => {
|
|
9
|
+
acc[`${prefix}.${key}`] = translations[key];
|
|
10
|
+
return acc;
|
|
11
|
+
}, {} as Record<string, any>);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 判断文件是否在 prefix 目录中(需要添加前缀)
|
|
16
|
+
* @param modulePath - 模块路径
|
|
17
|
+
* @returns 是否在 prefix 目录中
|
|
18
|
+
*/
|
|
19
|
+
const isInPrefixDir = (modulePath: string): boolean => {
|
|
20
|
+
// 检查路径中是否包含 /prefix/ 子串
|
|
21
|
+
return modulePath.includes('/prefix/') || modulePath.includes('\\prefix\\');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从路径中提取文件名(不含扩展名)
|
|
26
|
+
* @param modulePath - 模块路径
|
|
27
|
+
* @returns 文件名
|
|
28
|
+
*/
|
|
29
|
+
const extractFileName = (modulePath: string): string => {
|
|
30
|
+
const match = modulePath.match(/\/([^/]+)\.json$/);
|
|
31
|
+
return match?.[1] || '';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 处理单个语言目录下的所有翻译文件
|
|
36
|
+
* @param modules - 导入的模块对象
|
|
37
|
+
* @param locale - 语言代码
|
|
38
|
+
* @param basePath - 基础路径前缀(用于自定义目录)
|
|
39
|
+
* @returns 合并后的翻译对象
|
|
40
|
+
*/
|
|
41
|
+
const processLocaleModules = (
|
|
42
|
+
modules: Record<string, any>,
|
|
43
|
+
locale: string,
|
|
44
|
+
basePath = '.'
|
|
45
|
+
): Record<string, any> => {
|
|
46
|
+
const localePrefix = `${basePath}/${locale}/`;
|
|
47
|
+
|
|
48
|
+
return Object.keys(modules).reduce((acc, modulePath) => {
|
|
49
|
+
// 只处理指定语言目录下的文件
|
|
50
|
+
if (!modulePath.startsWith(localePrefix)) {
|
|
51
|
+
return acc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fileName = extractFileName(modulePath);
|
|
55
|
+
if (!fileName) {
|
|
56
|
+
return acc;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const translation = modules[modulePath] as Record<string, any>;
|
|
60
|
+
|
|
61
|
+
// 判断是否在 prefix 目录中,prefix 目录中的文件添加文件名作为前缀
|
|
62
|
+
if (isInPrefixDir(modulePath)) {
|
|
63
|
+
return {
|
|
64
|
+
...acc,
|
|
65
|
+
...addPrefix(translation, fileName)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 根目录文件,直接合并(不添加前缀)
|
|
70
|
+
return {
|
|
71
|
+
...acc,
|
|
72
|
+
...translation
|
|
73
|
+
};
|
|
74
|
+
}, {} as Record<string, any>);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 自动发现翻译目录下的所有语言
|
|
79
|
+
* @param modules - 导入的模块对象
|
|
80
|
+
* @param basePath - 基础路径前缀
|
|
81
|
+
* @returns 语言代码数组
|
|
82
|
+
*/
|
|
83
|
+
const discoverLocales = (modules: Record<string, any>, basePath = '.'): string[] => {
|
|
84
|
+
const locales = new Set<string>();
|
|
85
|
+
const pattern = new RegExp(`^${basePath.replace('.', '\\.')}/([^/]+)/`);
|
|
86
|
+
|
|
87
|
+
Object.keys(modules).forEach(modulePath => {
|
|
88
|
+
const match = modulePath.match(pattern);
|
|
89
|
+
if (match?.[1]) {
|
|
90
|
+
locales.add(match[1]);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return Array.from(locales);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 深度合并翻译对象(后面的值会覆盖前面的值)
|
|
99
|
+
* @param target - 目标对象(基础对象)
|
|
100
|
+
* @param source - 源对象(覆盖对象,优先级更高)
|
|
101
|
+
* @returns 合并后的新对象
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const base = { a: { b: 1, c: 2 }, d: 3 };
|
|
105
|
+
* const override = { a: { b: 10 }, e: 4 };
|
|
106
|
+
* const result = deepMerge(base, override);
|
|
107
|
+
* // 结果:{ a: { b: 10, c: 2 }, d: 3, e: 4 }
|
|
108
|
+
* // 说明:override.a.b 覆盖了 base.a.b,但 base.a.c 保留
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
const deepMerge = (
|
|
112
|
+
target: Record<string, any>,
|
|
113
|
+
source: Record<string, any>
|
|
114
|
+
): Record<string, any> => {
|
|
115
|
+
// 创建新对象,避免修改原始对象
|
|
116
|
+
const result = { ...target };
|
|
117
|
+
|
|
118
|
+
// 遍历源对象的所有键
|
|
119
|
+
for (const key in source) {
|
|
120
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
121
|
+
const sourceValue = source[key];
|
|
122
|
+
const targetValue = result[key];
|
|
123
|
+
|
|
124
|
+
// 判断是否需要深度合并(两者都是对象且不是数组)
|
|
125
|
+
const shouldDeepMerge =
|
|
126
|
+
typeof sourceValue === 'object' &&
|
|
127
|
+
sourceValue !== null &&
|
|
128
|
+
!Array.isArray(sourceValue) &&
|
|
129
|
+
typeof targetValue === 'object' &&
|
|
130
|
+
targetValue !== null &&
|
|
131
|
+
!Array.isArray(targetValue);
|
|
132
|
+
|
|
133
|
+
if (shouldDeepMerge) {
|
|
134
|
+
// 递归深度合并对象
|
|
135
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
136
|
+
} else {
|
|
137
|
+
// 直接覆盖(包括基本类型、数组、null、undefined)
|
|
138
|
+
result[key] = sourceValue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 翻译加载器配置接口
|
|
148
|
+
*/
|
|
149
|
+
interface TranslationsConfig {
|
|
150
|
+
/** 扩展翻译目录配置数组(自定义目录的值会覆盖默认目录的值) */
|
|
151
|
+
extensions?: Array<{
|
|
152
|
+
/** 扩展目录的模块对象(使用 import.meta.glob 导入) */
|
|
153
|
+
modules: Record<string, any>;
|
|
154
|
+
/** 扩展目录的基础路径(用于路径匹配) */
|
|
155
|
+
basePath?: string;
|
|
156
|
+
}>;
|
|
157
|
+
/** 指定要加载的语言列表,如果不指定则自动发现 */
|
|
158
|
+
locales?: string[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 创建多语言翻译对象
|
|
163
|
+
* @param config - 翻译加载器配置(可选)
|
|
164
|
+
* @returns 以语言为 key 的翻译对象
|
|
165
|
+
*
|
|
166
|
+
* @description
|
|
167
|
+
* 该方法实现了多目录翻译文件的深度合并机制:
|
|
168
|
+
* 1. 自动加载默认目录 (translations/) 下的 JSON 文件
|
|
169
|
+
* 2. 支持扩展自定义目录,可以添加多个自定义来源
|
|
170
|
+
* 3. 合并顺序:默认目录 → 自定义目录 1 → 自定义目录 2 → ...
|
|
171
|
+
* 4. 覆盖机制:后面的目录会 **深度覆盖**前面目录的值
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* // 基础用法:只加载默认目录
|
|
176
|
+
* const t = translations();
|
|
177
|
+
*
|
|
178
|
+
* // 高级用法:加载默认目录 + 自定义扩展目录(自定义目录会深度覆盖默认目录)
|
|
179
|
+
* const t = translations({
|
|
180
|
+
* extensions: [
|
|
181
|
+
* {
|
|
182
|
+
* modules: import.meta.glob('../custom/ * \/ ** \/ *.json', { eager: true, import: 'default' }),
|
|
183
|
+
* basePath: '../custom'
|
|
184
|
+
* }
|
|
185
|
+
* ]
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* // 指定语言列表
|
|
189
|
+
* const t = translations({
|
|
190
|
+
* locales: ['en', 'zh-Hans', 'zh-Hant']
|
|
191
|
+
* });
|
|
192
|
+
*
|
|
193
|
+
* // 深度覆盖示例:
|
|
194
|
+
* // 默认目录的 en.json: { common: { hello: "Hello", world: "World" }, home: { title: "Home" } }
|
|
195
|
+
* // 自定义目录的 en.json: { common: { hello: "Hi" }, about: { title: "About" } }
|
|
196
|
+
* // 最终结果: { common: { hello: "Hi", world: "World" }, home: { title: "Home" }, about: { title: "About" } }
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export const translations = (config?: TranslationsConfig): Record<string, Record<string, any>> => {
|
|
200
|
+
const { extensions = [], locales: specifiedLocales } = config || {};
|
|
201
|
+
|
|
202
|
+
// @ts-ignore
|
|
203
|
+
// 自动加载默认翻译目录
|
|
204
|
+
const defaultModules = import.meta.glob('./*/**/*.json', { eager: true, import: 'default' });
|
|
205
|
+
|
|
206
|
+
// 收集所有模块(默认目录在前,自定义目录在后,确保自定义目录覆盖默认目录)
|
|
207
|
+
const allModuleSources = [
|
|
208
|
+
{ modules: defaultModules, basePath: '.' },
|
|
209
|
+
...extensions
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// 自动发现所有语言或使用指定的语言列表
|
|
213
|
+
const allLocales = specifiedLocales || [
|
|
214
|
+
...new Set(
|
|
215
|
+
allModuleSources.flatMap(({ modules, basePath = '.' }) =>
|
|
216
|
+
discoverLocales(modules, basePath)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
// 为每个语言构建翻译对象
|
|
222
|
+
return allLocales.reduce((result, locale) => {
|
|
223
|
+
// 合并所有来源的翻译(后面的会覆盖前面的)
|
|
224
|
+
result[locale] = allModuleSources.reduce((acc, {modules, basePath = '.'}) => {
|
|
225
|
+
const localeTranslations = processLocaleModules(modules, locale, basePath);
|
|
226
|
+
return deepMerge(acc, localeTranslations);
|
|
227
|
+
}, {} as Record<string, any>);
|
|
228
|
+
return result;
|
|
229
|
+
}, {} as Record<string, Record<string, any>>);
|
|
230
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"website.plugin.name": "网站管理",
|
|
3
|
+
"website.widget.title": "仪表盘",
|
|
4
|
+
"website.widget.link": "前往内容管理",
|
|
5
|
+
"website.menu.dashboard": "Dashboard 主页",
|
|
6
|
+
"website.menu.inquiry": "询盘列表",
|
|
7
|
+
"website.menu.visitor": "访客记录",
|
|
8
|
+
"website.fields.slugInput.label": "URL别名",
|
|
9
|
+
"website.fields.slugInput.description": "输入URL友好的别名",
|
|
10
|
+
"website.fields.slugInput.available": "可用",
|
|
11
|
+
"website.fields.slugInput.unavailable": "不可用",
|
|
12
|
+
"website.fields.slugInput.regenerate": "重新生成",
|
|
13
|
+
"website.slug-input.input.aria-label": "Slug 输入框",
|
|
14
|
+
"website.slug-input.input.placeholder": "输入文本将自动转换为 slug",
|
|
15
|
+
"website.slug-input.preview.label": "Slug 预览:",
|
|
16
|
+
"website.slug-input.check-availability.error": "检查可用性失败",
|
|
17
|
+
"website.color-picker.toggle.aria-label": "颜色选择器切换",
|
|
18
|
+
"website.color-picker.input.format": "HEX",
|
|
19
|
+
"website.color-picker.input.aria-label": "颜色选择器输入",
|
|
20
|
+
"website.icon-picker.toggle.aria-label": "图标选择器切换",
|
|
21
|
+
"website.icon-picker.placeholder": "选择图标",
|
|
22
|
+
"website.icon-picker.title": "选择图标",
|
|
23
|
+
"website.icon-picker.back": "返回",
|
|
24
|
+
"website.icon-picker.search": "搜索图标...",
|
|
25
|
+
"website.icon-picker.loading": "加载中...",
|
|
26
|
+
"website.icon-picker.no-icons": "未找到图标"
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"website.plugin.name": "网站管理",
|
|
3
|
+
"website.widget.title": "仪表盘",
|
|
4
|
+
"website.widget.link": "前往内容管理",
|
|
5
|
+
"website.menu.dashboard": "Dashboard 主页",
|
|
6
|
+
"website.menu.inquiry": "询盘列表",
|
|
7
|
+
"website.menu.visitor": "访客记录",
|
|
8
|
+
"website.fields.slugInput.label": "URL别名",
|
|
9
|
+
"website.fields.slugInput.description": "输入URL友好的别名",
|
|
10
|
+
"website.fields.slugInput.available": "可用",
|
|
11
|
+
"website.fields.slugInput.unavailable": "不可用",
|
|
12
|
+
"website.fields.slugInput.regenerate": "重新生成",
|
|
13
|
+
"website.slug-input.input.aria-label": "Slug 输入框",
|
|
14
|
+
"website.slug-input.input.placeholder": "输入文本将自动转换为 slug",
|
|
15
|
+
"website.slug-input.preview.label": "Slug 预览:",
|
|
16
|
+
"website.slug-input.check-availability.error": "检查可用性失败",
|
|
17
|
+
"website.color-picker.toggle.aria-label": "颜色选择器切换",
|
|
18
|
+
"website.color-picker.input.format": "HEX",
|
|
19
|
+
"website.color-picker.input.aria-label": "颜色选择器输入",
|
|
20
|
+
"website.icon-picker.toggle.aria-label": "图标选择器切换",
|
|
21
|
+
"website.icon-picker.placeholder": "选择图标",
|
|
22
|
+
"website.icon-picker.title": "选择图标",
|
|
23
|
+
"website.icon-picker.back": "返回",
|
|
24
|
+
"website.icon-picker.search": "搜索图标...",
|
|
25
|
+
"website.icon-picker.loading": "加载中...",
|
|
26
|
+
"website.icon-picker.no-icons": "未找到图标"
|
|
27
|
+
}
|