@rettangoli/fe 1.1.3 → 1.2.0
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.md +8 -0
- package/package.json +1 -1
- package/src/cli/build.js +10 -0
- package/src/cli/check.js +6 -1
- package/src/cli/contracts.js +32 -6
- package/src/cli/frontendEntrySource.js +54 -2
- package/src/cli/i18nBuild.js +367 -0
- package/src/cli/vitePlugin.js +68 -0
- package/src/cli/watch.js +2 -0
- package/src/core/contracts/componentFiles.js +10 -1
- package/src/core/i18n/viewReferences.js +287 -0
- package/src/core/runtime/componentOrchestrator.js +1 -0
- package/src/core/runtime/events.js +7 -0
- package/src/core/runtime/globalListeners.js +2 -0
- package/src/core/runtime/i18n.js +155 -0
- package/src/core/runtime/lifecycle.js +14 -1
- package/src/core/runtime/store.js +11 -3
- package/src/index.js +2 -0
- package/src/parser.js +1 -0
- package/src/web/createWebComponentClass.js +28 -2
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ rtgl fe watch # Start dev server
|
|
|
26
26
|
- **[Schema System](./docs/schema.md)** - Component API and metadata
|
|
27
27
|
- **[Store Management](./docs/store.md)** - State patterns
|
|
28
28
|
- **[Event Handlers](./docs/handlers.md)** - Event handling
|
|
29
|
+
- **[Internationalization](./docs/i18n.md)** - Locale files, view usage, and locale switching
|
|
29
30
|
|
|
30
31
|
## Architecture
|
|
31
32
|
|
|
@@ -123,6 +124,13 @@ fe:
|
|
|
123
124
|
- "./src/pages"
|
|
124
125
|
setup: "setup.js"
|
|
125
126
|
outfile: "./dist/bundle.js"
|
|
127
|
+
i18n:
|
|
128
|
+
dir: "./src/i18n"
|
|
129
|
+
defaultLocale: "en"
|
|
130
|
+
fallbackLocale: "en"
|
|
131
|
+
locales:
|
|
132
|
+
- "en"
|
|
133
|
+
- "vi"
|
|
126
134
|
examples:
|
|
127
135
|
outputDir: "./vt/specs/examples"
|
|
128
136
|
```
|
package/package.json
CHANGED
package/src/cli/build.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
|
|
7
7
|
createRettangoliFeVitePlugin,
|
|
8
8
|
} from "./vitePlugin.js";
|
|
9
|
+
import { emitI18nAssets, loadI18nBuildContext } from "./i18nBuild.js";
|
|
9
10
|
|
|
10
11
|
const buildRettangoliFrontend = async (options = {}) => {
|
|
11
12
|
console.log("running build with options", options);
|
|
@@ -16,12 +17,18 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
16
17
|
outfile = "./vt/static/main.js",
|
|
17
18
|
setup = "setup.js",
|
|
18
19
|
development = false,
|
|
20
|
+
i18n = null,
|
|
19
21
|
} = options;
|
|
20
22
|
|
|
21
23
|
const resolvedOutfile = path.resolve(cwd, outfile);
|
|
22
24
|
const outDir = path.dirname(resolvedOutfile);
|
|
23
25
|
const outFileName = path.basename(resolvedOutfile);
|
|
24
26
|
const relativeOutDir = path.relative(cwd, outDir) || ".";
|
|
27
|
+
const i18nContext = loadI18nBuildContext({
|
|
28
|
+
cwd,
|
|
29
|
+
i18n,
|
|
30
|
+
errorPrefix: "[Build]",
|
|
31
|
+
});
|
|
25
32
|
|
|
26
33
|
await viteBuild({
|
|
27
34
|
configFile: false,
|
|
@@ -31,6 +38,7 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
31
38
|
cwd,
|
|
32
39
|
dirs,
|
|
33
40
|
setup,
|
|
41
|
+
i18n,
|
|
34
42
|
errorPrefix: "[Build]",
|
|
35
43
|
}),
|
|
36
44
|
],
|
|
@@ -52,6 +60,8 @@ const buildRettangoliFrontend = async (options = {}) => {
|
|
|
52
60
|
},
|
|
53
61
|
});
|
|
54
62
|
|
|
63
|
+
emitI18nAssets({ outDir, i18nContext });
|
|
64
|
+
|
|
55
65
|
console.log(`Build complete. Output file: ${resolvedOutfile}`);
|
|
56
66
|
};
|
|
57
67
|
|
package/src/cli/check.js
CHANGED
|
@@ -9,11 +9,16 @@ const checkRettangoliFrontend = (options = {}) => {
|
|
|
9
9
|
cwd = process.cwd(),
|
|
10
10
|
dirs = ["./example"],
|
|
11
11
|
format = "text",
|
|
12
|
+
i18n = null,
|
|
12
13
|
} = options;
|
|
13
14
|
const outputFormat = format === "json" ? "json" : "text";
|
|
14
15
|
|
|
15
16
|
const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
|
|
16
|
-
const { errors, summary, index } = analyzeComponentDirs({
|
|
17
|
+
const { errors, summary, index } = analyzeComponentDirs({
|
|
18
|
+
dirs: resolvedDirs,
|
|
19
|
+
cwd,
|
|
20
|
+
i18n,
|
|
21
|
+
});
|
|
17
22
|
|
|
18
23
|
if (errors.length > 0) {
|
|
19
24
|
if (outputFormat === "json") {
|
package/src/cli/contracts.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
formatContractErrors as formatContractErrorLines,
|
|
7
7
|
validateComponentContractIndex,
|
|
8
8
|
} from "../core/contracts/componentFiles.js";
|
|
9
|
+
import { analyzeI18nBuildContext } from "./i18nBuild.js";
|
|
9
10
|
|
|
10
11
|
export const SUPPORTED_COMPONENT_FILE_SUFFIXES = Object.freeze([
|
|
11
12
|
".store.js",
|
|
@@ -54,9 +55,10 @@ export const collectComponentContractEntriesFromDirs = (dirs = []) => {
|
|
|
54
55
|
export const validateComponentEntries = ({
|
|
55
56
|
entries = [],
|
|
56
57
|
errorPrefix = "[Check]",
|
|
58
|
+
i18nContext = { enabled: false },
|
|
57
59
|
}) => {
|
|
58
60
|
const index = buildComponentContractIndex(entries);
|
|
59
|
-
const errors = validateComponentContractIndex(index);
|
|
61
|
+
const errors = validateComponentContractIndex(index, { i18nContext });
|
|
60
62
|
if (errors.length > 0) {
|
|
61
63
|
throw new Error(
|
|
62
64
|
`${errorPrefix} Component contract validation failed:\n${formatContractErrorLines(errors).join("\n")}`,
|
|
@@ -71,9 +73,14 @@ export const validateComponentEntries = ({
|
|
|
71
73
|
export const validateComponentDirs = ({
|
|
72
74
|
dirs = [],
|
|
73
75
|
errorPrefix = "[Check]",
|
|
76
|
+
i18nContext = { enabled: false },
|
|
74
77
|
}) => {
|
|
75
78
|
const entries = collectComponentContractEntriesFromDirs(dirs);
|
|
76
|
-
const validationResult = validateComponentEntries({
|
|
79
|
+
const validationResult = validateComponentEntries({
|
|
80
|
+
entries,
|
|
81
|
+
errorPrefix,
|
|
82
|
+
i18nContext,
|
|
83
|
+
});
|
|
77
84
|
return {
|
|
78
85
|
entries,
|
|
79
86
|
...validationResult,
|
|
@@ -131,9 +138,16 @@ export const formatContractFailureReport = ({
|
|
|
131
138
|
].join("\n");
|
|
132
139
|
};
|
|
133
140
|
|
|
134
|
-
export const analyzeComponentEntries = ({
|
|
141
|
+
export const analyzeComponentEntries = ({
|
|
142
|
+
entries = [],
|
|
143
|
+
i18nContext = { enabled: false },
|
|
144
|
+
i18nErrors = [],
|
|
145
|
+
}) => {
|
|
135
146
|
const index = buildComponentContractIndex(entries);
|
|
136
|
-
const errors =
|
|
147
|
+
const errors = [
|
|
148
|
+
...i18nErrors,
|
|
149
|
+
...validateComponentContractIndex(index, { i18nContext }),
|
|
150
|
+
];
|
|
137
151
|
const summary = summarizeContractErrors(errors);
|
|
138
152
|
return {
|
|
139
153
|
entries,
|
|
@@ -143,7 +157,19 @@ export const analyzeComponentEntries = ({ entries = [] }) => {
|
|
|
143
157
|
};
|
|
144
158
|
};
|
|
145
159
|
|
|
146
|
-
export const analyzeComponentDirs = ({
|
|
160
|
+
export const analyzeComponentDirs = ({
|
|
161
|
+
dirs = [],
|
|
162
|
+
cwd = process.cwd(),
|
|
163
|
+
i18n = null,
|
|
164
|
+
}) => {
|
|
147
165
|
const entries = collectComponentContractEntriesFromDirs(dirs);
|
|
148
|
-
|
|
166
|
+
const { context: i18nContext, errors: i18nErrors } = analyzeI18nBuildContext({
|
|
167
|
+
cwd,
|
|
168
|
+
i18n,
|
|
169
|
+
});
|
|
170
|
+
return analyzeComponentEntries({
|
|
171
|
+
entries,
|
|
172
|
+
i18nContext,
|
|
173
|
+
i18nErrors,
|
|
174
|
+
});
|
|
149
175
|
};
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
isSupportedComponentFile,
|
|
10
10
|
validateComponentEntries,
|
|
11
11
|
} from "./contracts.js";
|
|
12
|
+
import {
|
|
13
|
+
buildI18nAssets,
|
|
14
|
+
loadI18nBuildContext,
|
|
15
|
+
} from "./i18nBuild.js";
|
|
12
16
|
|
|
13
17
|
const MODULE_FILE_TYPES = new Set(["handlers", "store", "methods"]);
|
|
14
18
|
const YAML_FILE_TYPES = new Set(["view", "constants", "schema"]);
|
|
@@ -65,15 +69,52 @@ const createComponentImportLines = ({ componentMatrix, categories }) => {
|
|
|
65
69
|
return lines.join("\n");
|
|
66
70
|
};
|
|
67
71
|
|
|
72
|
+
const createI18nRuntimeSource = ({ i18nContext }) => {
|
|
73
|
+
if (!i18nContext.enabled) {
|
|
74
|
+
return "const __rtglFrameworkDeps = {};";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const assets = buildI18nAssets({ i18nContext });
|
|
78
|
+
const urlEntries = assets
|
|
79
|
+
.map((asset) => {
|
|
80
|
+
return ` ${JSON.stringify(asset.locale)}: new URL(/* @vite-ignore */ ${JSON.stringify(`./${asset.relativeFileName}`)}, import.meta.url).href,`;
|
|
81
|
+
})
|
|
82
|
+
.join("\n");
|
|
83
|
+
|
|
84
|
+
const initialCatalogs = {};
|
|
85
|
+
initialCatalogs[i18nContext.defaultLocale] =
|
|
86
|
+
i18nContext.catalogs[i18nContext.defaultLocale];
|
|
87
|
+
initialCatalogs[i18nContext.fallbackLocale] =
|
|
88
|
+
i18nContext.catalogs[i18nContext.fallbackLocale];
|
|
89
|
+
|
|
90
|
+
return `
|
|
91
|
+
const __rtglI18nRuntime = createI18nRuntime({
|
|
92
|
+
defaultLocale: ${JSON.stringify(i18nContext.defaultLocale)},
|
|
93
|
+
fallbackLocale: ${JSON.stringify(i18nContext.fallbackLocale)},
|
|
94
|
+
locales: ${JSON.stringify(i18nContext.locales)},
|
|
95
|
+
urls: {
|
|
96
|
+
${urlEntries}
|
|
97
|
+
},
|
|
98
|
+
initialCatalogs: ${JSON.stringify(initialCatalogs)},
|
|
99
|
+
});
|
|
100
|
+
await __rtglI18nRuntime.ready();
|
|
101
|
+
const __rtglFrameworkDeps = {
|
|
102
|
+
__rtglI18nRuntime,
|
|
103
|
+
locale: __rtglI18nRuntime.locale,
|
|
104
|
+
};`.trim();
|
|
105
|
+
};
|
|
106
|
+
|
|
68
107
|
export const generateFrontendEntrySource = ({
|
|
69
108
|
cwd = process.cwd(),
|
|
70
109
|
dirs = ["./example"],
|
|
71
110
|
setup = "setup.js",
|
|
72
111
|
command = "build",
|
|
112
|
+
i18n = null,
|
|
73
113
|
errorPrefix = "[Build]",
|
|
74
114
|
} = {}) => {
|
|
75
115
|
const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
|
|
76
116
|
const resolvedSetup = path.resolve(cwd, setup);
|
|
117
|
+
const i18nContext = loadI18nBuildContext({ cwd, i18n, errorPrefix });
|
|
77
118
|
const allFiles = getAllFiles(resolvedDirs)
|
|
78
119
|
.filter((filePath) => isSupportedComponentFile(filePath))
|
|
79
120
|
.sort((a, b) => a.localeCompare(b));
|
|
@@ -143,6 +184,7 @@ export const generateFrontendEntrySource = ({
|
|
|
143
184
|
validateComponentEntries({
|
|
144
185
|
entries: componentContractEntries,
|
|
145
186
|
errorPrefix,
|
|
187
|
+
i18nContext,
|
|
146
188
|
});
|
|
147
189
|
|
|
148
190
|
const setupImportPath = toImportPath({
|
|
@@ -153,18 +195,28 @@ export const generateFrontendEntrySource = ({
|
|
|
153
195
|
componentMatrix,
|
|
154
196
|
categories: Object.keys(componentMatrix).sort(),
|
|
155
197
|
});
|
|
198
|
+
const feImports = i18nContext.enabled
|
|
199
|
+
? "createComponent, createI18nRuntime"
|
|
200
|
+
: "createComponent";
|
|
201
|
+
const i18nRuntimeSource = createI18nRuntimeSource({ i18nContext });
|
|
156
202
|
|
|
157
203
|
return `
|
|
158
204
|
${declarationLines.join("\n")}
|
|
159
|
-
import {
|
|
205
|
+
import { ${feImports} } from "@rettangoli/fe";
|
|
160
206
|
import { deps } from ${JSON.stringify(setupImportPath)};
|
|
161
207
|
|
|
162
208
|
${categoryLines}
|
|
163
209
|
|
|
210
|
+
${i18nRuntimeSource}
|
|
211
|
+
|
|
164
212
|
Object.keys(imports).forEach((category) => {
|
|
165
213
|
Object.keys(imports[category]).forEach((component) => {
|
|
166
214
|
const componentConfig = imports[category][component];
|
|
167
|
-
const
|
|
215
|
+
const categoryDeps = {
|
|
216
|
+
...((deps && deps[category]) || {}),
|
|
217
|
+
...__rtglFrameworkDeps,
|
|
218
|
+
};
|
|
219
|
+
const webComponent = createComponent({ ...componentConfig }, categoryDeps);
|
|
168
220
|
const elementName = componentConfig.schema?.componentName;
|
|
169
221
|
if (!elementName) {
|
|
170
222
|
throw new Error(
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
import { load as loadYaml } from "js-yaml";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_OUTPUT_DIR = "i18n";
|
|
12
|
+
const LOCALE_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
|
|
14
|
+
const isPlainObject = (value) =>
|
|
15
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
|
|
17
|
+
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
18
|
+
|
|
19
|
+
const createI18nError = ({ code = "RTGL-I18N-004", message, filePath }) => ({
|
|
20
|
+
code,
|
|
21
|
+
message,
|
|
22
|
+
filePath: filePath || "",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const assertNoI18nErrors = ({ errors, errorPrefix }) => {
|
|
26
|
+
if (errors.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const details = errors
|
|
31
|
+
.map((error) => `${error.code} ${error.message}${error.filePath ? ` [${error.filePath}]` : ""}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
throw new Error(`${errorPrefix} i18n validation failed:\n${details}`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const validateLocaleName = ({ locale, errors, filePath }) => {
|
|
37
|
+
if (typeof locale !== "string" || !LOCALE_PATTERN.test(locale)) {
|
|
38
|
+
errors.push(createI18nError({
|
|
39
|
+
message: `invalid locale name ${JSON.stringify(locale)}. Locale names may contain letters, numbers, "_" and "-".`,
|
|
40
|
+
filePath,
|
|
41
|
+
}));
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const flattenCatalogKeys = (catalog = {}) => {
|
|
48
|
+
const keys = [];
|
|
49
|
+
Object.entries(catalog).forEach(([namespace, messages]) => {
|
|
50
|
+
Object.keys(messages || {}).forEach((key) => {
|
|
51
|
+
keys.push(`${namespace}.${key}`);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return keys.sort();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const validateCatalog = ({ catalog, locale, filePath }) => {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const normalized = {};
|
|
60
|
+
|
|
61
|
+
if (!isPlainObject(catalog)) {
|
|
62
|
+
errors.push(createI18nError({
|
|
63
|
+
message: `${locale}.yaml must contain a YAML object at the root.`,
|
|
64
|
+
filePath,
|
|
65
|
+
}));
|
|
66
|
+
return { catalog: normalized, errors };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Object.entries(catalog).forEach(([namespace, messages]) => {
|
|
70
|
+
if (!isPlainObject(messages)) {
|
|
71
|
+
errors.push(createI18nError({
|
|
72
|
+
message: `${locale}.yaml namespace "${namespace}" must be an object. Use namespace -> key -> content.`,
|
|
73
|
+
filePath,
|
|
74
|
+
}));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
normalized[namespace] = {};
|
|
79
|
+
Object.entries(messages).forEach(([key, content]) => {
|
|
80
|
+
if (isPlainObject(content) || Array.isArray(content)) {
|
|
81
|
+
errors.push(createI18nError({
|
|
82
|
+
message: `${locale}.yaml key "${namespace}.${key}" is nested too deeply. Use namespace -> key -> content only.`,
|
|
83
|
+
filePath,
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof content !== "string") {
|
|
89
|
+
errors.push(createI18nError({
|
|
90
|
+
message: `${locale}.yaml key "${namespace}.${key}" must be a string.`,
|
|
91
|
+
filePath,
|
|
92
|
+
}));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
normalized[namespace][key] = content;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return { catalog: normalized, errors };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const normalizeI18nConfig = ({ cwd, i18n }) => {
|
|
104
|
+
const errors = [];
|
|
105
|
+
|
|
106
|
+
if (!i18n) {
|
|
107
|
+
return {
|
|
108
|
+
config: { enabled: false },
|
|
109
|
+
errors,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isPlainObject(i18n)) {
|
|
114
|
+
errors.push(createI18nError({
|
|
115
|
+
message: "fe.i18n must be an object.",
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
config: { enabled: false },
|
|
119
|
+
errors,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const dir = i18n.dir;
|
|
124
|
+
if (typeof dir !== "string" || dir.trim() === "") {
|
|
125
|
+
errors.push(createI18nError({
|
|
126
|
+
message: "fe.i18n.dir must be a non-empty string.",
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const defaultLocale = i18n.defaultLocale;
|
|
131
|
+
const fallbackLocale = i18n.fallbackLocale;
|
|
132
|
+
validateLocaleName({ locale: defaultLocale, errors });
|
|
133
|
+
validateLocaleName({ locale: fallbackLocale, errors });
|
|
134
|
+
|
|
135
|
+
if (!Array.isArray(i18n.locales) || i18n.locales.length === 0) {
|
|
136
|
+
errors.push(createI18nError({
|
|
137
|
+
message: "fe.i18n.locales must be a non-empty array.",
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const locales = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
if (Array.isArray(i18n.locales)) {
|
|
144
|
+
i18n.locales.forEach((locale) => {
|
|
145
|
+
if (!validateLocaleName({ locale, errors })) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (seen.has(locale)) {
|
|
149
|
+
errors.push(createI18nError({
|
|
150
|
+
message: `fe.i18n.locales contains duplicate locale "${locale}".`,
|
|
151
|
+
}));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
seen.add(locale);
|
|
155
|
+
locales.push(locale);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (defaultLocale && locales.length > 0 && !seen.has(defaultLocale)) {
|
|
160
|
+
errors.push(createI18nError({
|
|
161
|
+
message: `fe.i18n.defaultLocale "${defaultLocale}" must be listed in fe.i18n.locales.`,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (fallbackLocale && locales.length > 0 && !seen.has(fallbackLocale)) {
|
|
166
|
+
errors.push(createI18nError({
|
|
167
|
+
message: `fe.i18n.fallbackLocale "${fallbackLocale}" must be listed in fe.i18n.locales.`,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const outputDir = typeof i18n.outputDir === "string" && i18n.outputDir.trim()
|
|
172
|
+
? i18n.outputDir
|
|
173
|
+
: DEFAULT_OUTPUT_DIR;
|
|
174
|
+
const normalizedOutputDir = outputDir.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
175
|
+
if (
|
|
176
|
+
!normalizedOutputDir ||
|
|
177
|
+
normalizedOutputDir.split("/").includes("..") ||
|
|
178
|
+
path.posix.isAbsolute(normalizedOutputDir)
|
|
179
|
+
) {
|
|
180
|
+
errors.push(createI18nError({
|
|
181
|
+
message: "fe.i18n.outputDir must be a relative path when provided.",
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
config: {
|
|
187
|
+
enabled: errors.length === 0,
|
|
188
|
+
dir,
|
|
189
|
+
resolvedDir: dir ? path.resolve(cwd, dir) : null,
|
|
190
|
+
outputDir: normalizedOutputDir || DEFAULT_OUTPUT_DIR,
|
|
191
|
+
defaultLocale,
|
|
192
|
+
fallbackLocale,
|
|
193
|
+
locales,
|
|
194
|
+
},
|
|
195
|
+
errors,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const analyzeI18nBuildContext = ({
|
|
200
|
+
cwd = process.cwd(),
|
|
201
|
+
i18n = null,
|
|
202
|
+
} = {}) => {
|
|
203
|
+
const { config, errors } = normalizeI18nConfig({ cwd, i18n });
|
|
204
|
+
|
|
205
|
+
if (!i18n || errors.length > 0) {
|
|
206
|
+
return {
|
|
207
|
+
context: { enabled: false },
|
|
208
|
+
errors,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!existsSync(config.resolvedDir)) {
|
|
213
|
+
errors.push(createI18nError({
|
|
214
|
+
message: `fe.i18n.dir does not exist: ${config.resolvedDir}`,
|
|
215
|
+
filePath: config.resolvedDir,
|
|
216
|
+
}));
|
|
217
|
+
return {
|
|
218
|
+
context: { enabled: false },
|
|
219
|
+
errors,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const localeFiles = {};
|
|
224
|
+
const catalogs = {};
|
|
225
|
+
|
|
226
|
+
config.locales.forEach((locale) => {
|
|
227
|
+
const filePath = path.join(config.resolvedDir, `${locale}.yaml`);
|
|
228
|
+
localeFiles[locale] = filePath;
|
|
229
|
+
|
|
230
|
+
if (!existsSync(filePath)) {
|
|
231
|
+
errors.push(createI18nError({
|
|
232
|
+
message: `missing i18n file for locale "${locale}". Expected ${filePath}.`,
|
|
233
|
+
filePath,
|
|
234
|
+
}));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let yamlObject;
|
|
239
|
+
try {
|
|
240
|
+
yamlObject = loadYaml(readFileSync(filePath, "utf8")) ?? {};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
errors.push(createI18nError({
|
|
243
|
+
message: `failed to parse ${locale}.yaml: ${error.message}`,
|
|
244
|
+
filePath,
|
|
245
|
+
}));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = validateCatalog({ catalog: yamlObject, locale, filePath });
|
|
250
|
+
errors.push(...result.errors);
|
|
251
|
+
catalogs[locale] = result.catalog;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const defaultCatalog = catalogs[config.defaultLocale];
|
|
255
|
+
if (defaultCatalog) {
|
|
256
|
+
const defaultKeys = flattenCatalogKeys(defaultCatalog);
|
|
257
|
+
config.locales.forEach((locale) => {
|
|
258
|
+
if (locale === config.defaultLocale || !catalogs[locale]) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
defaultKeys.forEach((fullKey) => {
|
|
263
|
+
const [namespace, key] = fullKey.split(".");
|
|
264
|
+
if (
|
|
265
|
+
!Object.prototype.hasOwnProperty.call(catalogs[locale] || {}, namespace) ||
|
|
266
|
+
!Object.prototype.hasOwnProperty.call(catalogs[locale]?.[namespace] || {}, key)
|
|
267
|
+
) {
|
|
268
|
+
errors.push(createI18nError({
|
|
269
|
+
message: `${locale}.yaml is missing key "${fullKey}" from default locale "${config.defaultLocale}".`,
|
|
270
|
+
filePath: localeFiles[locale],
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const context = {
|
|
278
|
+
...config,
|
|
279
|
+
enabled: errors.length === 0,
|
|
280
|
+
localeFiles,
|
|
281
|
+
catalogs,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
context,
|
|
286
|
+
errors,
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const loadI18nBuildContext = ({
|
|
291
|
+
cwd = process.cwd(),
|
|
292
|
+
i18n = null,
|
|
293
|
+
errorPrefix = "[Build]",
|
|
294
|
+
} = {}) => {
|
|
295
|
+
const { context, errors } = analyzeI18nBuildContext({ cwd, i18n });
|
|
296
|
+
assertNoI18nErrors({ errors, errorPrefix });
|
|
297
|
+
return context;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const buildI18nAssets = ({ i18nContext }) => {
|
|
301
|
+
if (!i18nContext?.enabled) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return i18nContext.locales.map((locale) => {
|
|
306
|
+
const relativeFileName = path.posix.join(
|
|
307
|
+
i18nContext.outputDir,
|
|
308
|
+
`${locale}.json`,
|
|
309
|
+
);
|
|
310
|
+
return {
|
|
311
|
+
locale,
|
|
312
|
+
relativeFileName,
|
|
313
|
+
sourcePath: i18nContext.localeFiles[locale],
|
|
314
|
+
catalog: i18nContext.catalogs[locale],
|
|
315
|
+
content: `${JSON.stringify(i18nContext.catalogs[locale], null, 2)}\n`,
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const emitI18nAssets = ({ outDir, i18nContext }) => {
|
|
321
|
+
const assets = buildI18nAssets({ i18nContext });
|
|
322
|
+
|
|
323
|
+
return assets.map((asset) => {
|
|
324
|
+
const outputPath = path.resolve(outDir, asset.relativeFileName);
|
|
325
|
+
mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
326
|
+
writeFileSync(outputPath, asset.content);
|
|
327
|
+
return {
|
|
328
|
+
...asset,
|
|
329
|
+
outputPath,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const isI18nSourceFilePath = ({ filePath, i18nContext }) => {
|
|
335
|
+
if (!i18nContext?.enabled && !i18nContext?.resolvedDir) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
340
|
+
const extension = path.extname(resolvedFilePath);
|
|
341
|
+
if (extension !== ".yaml") {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const relativePath = path.relative(i18nContext.resolvedDir, resolvedFilePath);
|
|
346
|
+
return (
|
|
347
|
+
relativePath !== "" &&
|
|
348
|
+
!relativePath.startsWith("..") &&
|
|
349
|
+
!path.isAbsolute(relativePath)
|
|
350
|
+
);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export const getI18nPublicAssetPath = ({
|
|
354
|
+
publicEntryPath,
|
|
355
|
+
relativeFileName,
|
|
356
|
+
}) => {
|
|
357
|
+
const normalizedEntry = publicEntryPath
|
|
358
|
+
? publicEntryPath.replace(/\\/g, "/")
|
|
359
|
+
: "/main.js";
|
|
360
|
+
const entryPath = normalizedEntry.startsWith("/")
|
|
361
|
+
? normalizedEntry
|
|
362
|
+
: `/${normalizedEntry}`;
|
|
363
|
+
return path.posix.join(
|
|
364
|
+
path.posix.dirname(entryPath),
|
|
365
|
+
toPosixPath(relativeFileName),
|
|
366
|
+
);
|
|
367
|
+
};
|