@rettangoli/fe 1.1.2 → 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/props.js +49 -0
- package/src/core/runtime/store.js +11 -3
- package/src/index.js +2 -0
- package/src/parser.js +1 -0
- package/src/web/componentDom.js +6 -1
- package/src/web/createWebComponentClass.js +28 -2
package/src/cli/vitePlugin.js
CHANGED
|
@@ -2,6 +2,12 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { isSupportedComponentFile } from "./contracts.js";
|
|
4
4
|
import { generateFrontendEntrySource } from "./frontendEntrySource.js";
|
|
5
|
+
import {
|
|
6
|
+
buildI18nAssets,
|
|
7
|
+
getI18nPublicAssetPath,
|
|
8
|
+
isI18nSourceFilePath,
|
|
9
|
+
loadI18nBuildContext,
|
|
10
|
+
} from "./i18nBuild.js";
|
|
5
11
|
|
|
6
12
|
export const RETTANGOLI_FE_VIRTUAL_ENTRY_ID = "virtual:rettangoli-fe-entry";
|
|
7
13
|
const RESOLVED_VIRTUAL_ENTRY_ID = `\0${RETTANGOLI_FE_VIRTUAL_ENTRY_ID}`;
|
|
@@ -22,16 +28,29 @@ const normalizePublicEntryPath = (value) => {
|
|
|
22
28
|
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
23
29
|
};
|
|
24
30
|
|
|
31
|
+
const getI18nWatchPaths = ({ i18nContext }) => {
|
|
32
|
+
if (!i18nContext?.enabled) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
i18nContext.resolvedDir,
|
|
38
|
+
...Object.values(i18nContext.localeFiles || {}),
|
|
39
|
+
].filter(Boolean);
|
|
40
|
+
};
|
|
41
|
+
|
|
25
42
|
export const createRettangoliFeVitePlugin = ({
|
|
26
43
|
cwd = process.cwd(),
|
|
27
44
|
dirs = ["./example"],
|
|
28
45
|
setup = "setup.js",
|
|
46
|
+
i18n = null,
|
|
29
47
|
errorPrefix = "[Build]",
|
|
30
48
|
publicEntryPath = null,
|
|
31
49
|
} = {}) => {
|
|
32
50
|
const resolvedDirs = dirs.map((directory) => path.resolve(cwd, directory));
|
|
33
51
|
const resolvedSetup = path.resolve(cwd, setup);
|
|
34
52
|
const normalizedPublicEntryPath = normalizePublicEntryPath(publicEntryPath);
|
|
53
|
+
const i18nContext = loadI18nBuildContext({ cwd, i18n, errorPrefix });
|
|
35
54
|
|
|
36
55
|
let currentCommand = "build";
|
|
37
56
|
let devServer = null;
|
|
@@ -42,6 +61,10 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
42
61
|
return true;
|
|
43
62
|
}
|
|
44
63
|
|
|
64
|
+
if (isI18nSourceFilePath({ filePath, i18nContext })) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
if (!isSupportedComponentFile(filePath)) {
|
|
46
69
|
return false;
|
|
47
70
|
}
|
|
@@ -91,12 +114,56 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
91
114
|
cwd,
|
|
92
115
|
dirs,
|
|
93
116
|
setup,
|
|
117
|
+
i18n,
|
|
94
118
|
command: currentCommand,
|
|
95
119
|
errorPrefix,
|
|
96
120
|
});
|
|
97
121
|
},
|
|
98
122
|
configureServer(server) {
|
|
99
123
|
devServer = server;
|
|
124
|
+
const i18nWatchPaths = getI18nWatchPaths({ i18nContext });
|
|
125
|
+
if (i18nWatchPaths.length > 0) {
|
|
126
|
+
server.watcher.add(i18nWatchPaths);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const serveI18nAsset = (req, res, next) => {
|
|
130
|
+
if (!i18nContext.enabled || !normalizedPublicEntryPath) {
|
|
131
|
+
next();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const reqPath = (req.url || "").split("?")[0];
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const currentI18nContext = loadI18nBuildContext({
|
|
139
|
+
cwd,
|
|
140
|
+
i18n,
|
|
141
|
+
errorPrefix,
|
|
142
|
+
});
|
|
143
|
+
const asset = buildI18nAssets({
|
|
144
|
+
i18nContext: currentI18nContext,
|
|
145
|
+
}).find((candidate) => {
|
|
146
|
+
const publicPath = getI18nPublicAssetPath({
|
|
147
|
+
publicEntryPath: normalizedPublicEntryPath,
|
|
148
|
+
relativeFileName: candidate.relativeFileName,
|
|
149
|
+
});
|
|
150
|
+
return reqPath === publicPath;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!asset) {
|
|
154
|
+
next();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
res.statusCode = 200;
|
|
159
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
160
|
+
res.end(asset.content);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
res.statusCode = 500;
|
|
163
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
164
|
+
res.end(error.stack || String(error));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
100
167
|
|
|
101
168
|
const onAdd = (filePath) => {
|
|
102
169
|
if (isTrackedFilePath(filePath)) {
|
|
@@ -114,6 +181,7 @@ export const createRettangoliFeVitePlugin = ({
|
|
|
114
181
|
server.watcher.on("unlink", onUnlink);
|
|
115
182
|
|
|
116
183
|
if (normalizedPublicEntryPath) {
|
|
184
|
+
server.middlewares.use(serveI18nAsset);
|
|
117
185
|
server.middlewares.use(async (req, res, next) => {
|
|
118
186
|
const reqPath = (req.url || "").split("?")[0];
|
|
119
187
|
if (reqPath !== normalizedPublicEntryPath) {
|
package/src/cli/watch.js
CHANGED
|
@@ -39,6 +39,7 @@ export const createWatchServer = async (options = {}) => {
|
|
|
39
39
|
port = 3001,
|
|
40
40
|
outfile = "./vt/static/main.js",
|
|
41
41
|
setup = "setup.js",
|
|
42
|
+
i18n = null,
|
|
42
43
|
} = options;
|
|
43
44
|
|
|
44
45
|
const { root, publicEntryPath } = resolveServeContext({ cwd, outfile });
|
|
@@ -56,6 +57,7 @@ export const createWatchServer = async (options = {}) => {
|
|
|
56
57
|
cwd,
|
|
57
58
|
dirs,
|
|
58
59
|
setup,
|
|
60
|
+
i18n,
|
|
59
61
|
errorPrefix: "[Watch]",
|
|
60
62
|
publicEntryPath,
|
|
61
63
|
}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findUnsupportedTemplatePropertyBindingSyntax } from "../view/templatePropertyBindings.js";
|
|
2
|
+
import { validateViewI18nReferences } from "../i18n/viewReferences.js";
|
|
2
3
|
|
|
3
4
|
export const FORBIDDEN_VIEW_KEYS = Object.freeze([
|
|
4
5
|
"elementName",
|
|
@@ -51,7 +52,8 @@ export const buildComponentContractIndex = (entries = []) => {
|
|
|
51
52
|
return index;
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
export const validateComponentContractIndex = (index = {}) => {
|
|
55
|
+
export const validateComponentContractIndex = (index = {}, options = {}) => {
|
|
56
|
+
const { i18nContext = { enabled: false } } = options;
|
|
55
57
|
const errors = [];
|
|
56
58
|
|
|
57
59
|
Object.entries(index).forEach(([category, components]) => {
|
|
@@ -90,6 +92,13 @@ export const validateComponentContractIndex = (index = {}) => {
|
|
|
90
92
|
filePath: viewFilePath || representativeFile,
|
|
91
93
|
});
|
|
92
94
|
}
|
|
95
|
+
|
|
96
|
+
errors.push(...validateViewI18nReferences({
|
|
97
|
+
viewYaml,
|
|
98
|
+
componentLabel,
|
|
99
|
+
filePath: viewFilePath || representativeFile,
|
|
100
|
+
i18nContext,
|
|
101
|
+
}));
|
|
93
102
|
});
|
|
94
103
|
});
|
|
95
104
|
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const EXPRESSION_PATTERN = /\$\{([^}]*)\}/g;
|
|
2
|
+
const I18N_PATH_PATTERN = /^i18n\.([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/;
|
|
3
|
+
const SIMPLE_I18N_PATH_PATTERN = /^i18n((?:\.[A-Za-z_$][\w$]*)+)$/;
|
|
4
|
+
|
|
5
|
+
const createIssue = ({ message, expression, path }) => ({
|
|
6
|
+
message,
|
|
7
|
+
expression,
|
|
8
|
+
path,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const createFindingKey = (finding) => `${finding.path}\u0000${finding.expression}`;
|
|
12
|
+
|
|
13
|
+
const extractTemplateExpressions = (text) => {
|
|
14
|
+
const expressions = [];
|
|
15
|
+
let match;
|
|
16
|
+
EXPRESSION_PATTERN.lastIndex = 0;
|
|
17
|
+
while ((match = EXPRESSION_PATTERN.exec(text)) !== null) {
|
|
18
|
+
expressions.push(match[1].trim());
|
|
19
|
+
}
|
|
20
|
+
return expressions;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const analyzeI18nExpression = ({ expression, path = "$" }) => {
|
|
24
|
+
const startsFromI18nRoot = /^i18n(?:\b|\.)/.test(expression);
|
|
25
|
+
const startsFromInternalI18nRoot = /^__rtglI18n(?:\b|\s*\()/.test(expression);
|
|
26
|
+
|
|
27
|
+
if (!startsFromI18nRoot && !startsFromInternalI18nRoot) {
|
|
28
|
+
return { references: [], issues: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
/^__rtglI18n\s*\(/.test(expression) ||
|
|
33
|
+
/^i18n\s*\(/.test(expression) ||
|
|
34
|
+
/^i18n(?:\.[A-Za-z_$][\w$]*)+\s*\(/.test(expression)
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
references: [],
|
|
38
|
+
issues: [
|
|
39
|
+
createIssue({
|
|
40
|
+
expression,
|
|
41
|
+
path,
|
|
42
|
+
message: "i18n function calls are not supported.",
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const exactPathMatch = expression.match(I18N_PATH_PATTERN);
|
|
49
|
+
if (exactPathMatch) {
|
|
50
|
+
return {
|
|
51
|
+
references: [
|
|
52
|
+
{
|
|
53
|
+
namespace: exactPathMatch[1],
|
|
54
|
+
key: exactPathMatch[2],
|
|
55
|
+
expression,
|
|
56
|
+
path,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
issues: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const simplePathMatch = expression.match(SIMPLE_I18N_PATH_PATTERN);
|
|
64
|
+
if (simplePathMatch) {
|
|
65
|
+
const segments = simplePathMatch[1].split(".").filter(Boolean);
|
|
66
|
+
const message = segments.length < 2
|
|
67
|
+
? "i18n references must use two levels: i18n.namespace.key."
|
|
68
|
+
: "i18n references must not be deeper than i18n.namespace.key.";
|
|
69
|
+
return {
|
|
70
|
+
references: [],
|
|
71
|
+
issues: [
|
|
72
|
+
createIssue({
|
|
73
|
+
expression,
|
|
74
|
+
path,
|
|
75
|
+
message,
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (startsFromI18nRoot || startsFromInternalI18nRoot) {
|
|
82
|
+
return {
|
|
83
|
+
references: [],
|
|
84
|
+
issues: [
|
|
85
|
+
createIssue({
|
|
86
|
+
expression,
|
|
87
|
+
path,
|
|
88
|
+
message: "i18n references must be direct paths: i18n.namespace.key.",
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { references: [], issues: [] };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const collectI18nReferencesFromView = ({
|
|
98
|
+
value,
|
|
99
|
+
path = "$",
|
|
100
|
+
references = [],
|
|
101
|
+
issues = [],
|
|
102
|
+
} = {}) => {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
const expressions = extractTemplateExpressions(value);
|
|
105
|
+
expressions.forEach((expression) => {
|
|
106
|
+
const result = analyzeI18nExpression({ expression, path });
|
|
107
|
+
references.push(...result.references);
|
|
108
|
+
issues.push(...result.issues);
|
|
109
|
+
});
|
|
110
|
+
return { references, issues };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
value.forEach((item, index) => {
|
|
115
|
+
collectI18nReferencesFromView({
|
|
116
|
+
value: item,
|
|
117
|
+
path: `${path}[${index}]`,
|
|
118
|
+
references,
|
|
119
|
+
issues,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
return { references, issues };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (value && typeof value === "object") {
|
|
126
|
+
Object.entries(value).forEach(([key, childValue]) => {
|
|
127
|
+
collectI18nReferencesFromView({
|
|
128
|
+
value: key,
|
|
129
|
+
path: `${path}.{key}`,
|
|
130
|
+
references,
|
|
131
|
+
issues,
|
|
132
|
+
});
|
|
133
|
+
collectI18nReferencesFromView({
|
|
134
|
+
value: childValue,
|
|
135
|
+
path: `${path}.${key}`,
|
|
136
|
+
references,
|
|
137
|
+
issues,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { references, issues };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const collectEventPayloadI18nReferences = ({
|
|
146
|
+
refs,
|
|
147
|
+
references,
|
|
148
|
+
issues,
|
|
149
|
+
}) => {
|
|
150
|
+
if (!refs || typeof refs !== "object" || Array.isArray(refs)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Object.entries(refs).forEach(([refKey, refConfig]) => {
|
|
155
|
+
const eventListeners = refConfig?.eventListeners;
|
|
156
|
+
if (!eventListeners || typeof eventListeners !== "object" || Array.isArray(eventListeners)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Object.entries(eventListeners).forEach(([eventType, eventConfig]) => {
|
|
161
|
+
if (
|
|
162
|
+
!eventConfig
|
|
163
|
+
|| typeof eventConfig !== "object"
|
|
164
|
+
|| Array.isArray(eventConfig)
|
|
165
|
+
|| !Object.prototype.hasOwnProperty.call(eventConfig, "payload")
|
|
166
|
+
) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
collectI18nReferencesFromView({
|
|
171
|
+
value: eventConfig.payload,
|
|
172
|
+
path: `$.refs.${refKey}.eventListeners.${eventType}.payload`,
|
|
173
|
+
references,
|
|
174
|
+
issues,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const collectRenderableI18nReferencesFromView = ({ viewYaml } = {}) => {
|
|
181
|
+
const references = [];
|
|
182
|
+
const issues = [];
|
|
183
|
+
|
|
184
|
+
if (viewYaml && typeof viewYaml === "object" && !Array.isArray(viewYaml)) {
|
|
185
|
+
if (Object.prototype.hasOwnProperty.call(viewYaml, "template")) {
|
|
186
|
+
collectI18nReferencesFromView({
|
|
187
|
+
value: viewYaml.template,
|
|
188
|
+
path: "$.template",
|
|
189
|
+
references,
|
|
190
|
+
issues,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
collectEventPayloadI18nReferences({
|
|
195
|
+
refs: viewYaml.refs,
|
|
196
|
+
references,
|
|
197
|
+
issues,
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
collectI18nReferencesFromView({
|
|
201
|
+
value: viewYaml,
|
|
202
|
+
references,
|
|
203
|
+
issues,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { references, issues };
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const validateViewI18nReferences = ({
|
|
211
|
+
viewYaml,
|
|
212
|
+
componentLabel,
|
|
213
|
+
filePath,
|
|
214
|
+
i18nContext = { enabled: false },
|
|
215
|
+
} = {}) => {
|
|
216
|
+
const supported = collectRenderableI18nReferencesFromView({
|
|
217
|
+
viewYaml,
|
|
218
|
+
});
|
|
219
|
+
const all = collectI18nReferencesFromView({
|
|
220
|
+
value: viewYaml,
|
|
221
|
+
});
|
|
222
|
+
const supportedFindingKeys = new Set([
|
|
223
|
+
...supported.references.map(createFindingKey),
|
|
224
|
+
...supported.issues.map(createFindingKey),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const unsupportedFindings = [
|
|
228
|
+
...all.references,
|
|
229
|
+
...all.issues,
|
|
230
|
+
].filter((finding) => !supportedFindingKeys.has(createFindingKey(finding)));
|
|
231
|
+
|
|
232
|
+
const errors = unsupportedFindings.map((finding) => ({
|
|
233
|
+
code: "RTGL-I18N-004",
|
|
234
|
+
message: `${componentLabel}: unsupported i18n expression "\${${finding.expression}}" at ${finding.path}. i18n references are supported only in template values, template bindings, and event listener payloads.`,
|
|
235
|
+
filePath,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
supported.issues.forEach((issue) => {
|
|
239
|
+
errors.push({
|
|
240
|
+
code: "RTGL-I18N-001",
|
|
241
|
+
message: `${componentLabel}: invalid i18n expression "\${${issue.expression}}": ${issue.message}`,
|
|
242
|
+
filePath,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const references = supported.references;
|
|
247
|
+
|
|
248
|
+
if (errors.length > 0) {
|
|
249
|
+
return errors;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!i18nContext?.enabled && references.length > 0) {
|
|
253
|
+
references.forEach((reference) => {
|
|
254
|
+
errors.push({
|
|
255
|
+
code: "RTGL-I18N-002",
|
|
256
|
+
message: `${componentLabel}: i18n reference "\${${reference.expression}}" requires fe.i18n configuration.`,
|
|
257
|
+
filePath,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
return errors;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (i18nContext?.enabled) {
|
|
264
|
+
const defaultCatalog = i18nContext.catalogs?.[i18nContext.defaultLocale] || {};
|
|
265
|
+
references.forEach((reference) => {
|
|
266
|
+
if (!Object.prototype.hasOwnProperty.call(defaultCatalog, reference.namespace)) {
|
|
267
|
+
errors.push({
|
|
268
|
+
code: "RTGL-I18N-003",
|
|
269
|
+
message: `${componentLabel}: missing i18n namespace "${reference.namespace}" in default locale "${i18nContext.defaultLocale}".`,
|
|
270
|
+
filePath,
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const namespaceMessages = defaultCatalog[reference.namespace] || {};
|
|
276
|
+
if (!Object.prototype.hasOwnProperty.call(namespaceMessages, reference.key)) {
|
|
277
|
+
errors.push({
|
|
278
|
+
code: "RTGL-I18N-003",
|
|
279
|
+
message: `${componentLabel}: missing i18n key "${reference.namespace}.${reference.key}" in default locale "${i18nContext.defaultLocale}".`,
|
|
280
|
+
filePath,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return errors;
|
|
287
|
+
};
|
|
@@ -16,6 +16,7 @@ export const createEventDispatchCallback = ({
|
|
|
16
16
|
handlers,
|
|
17
17
|
onMissingHandler,
|
|
18
18
|
parseAndRenderFn,
|
|
19
|
+
getPayloadContext,
|
|
19
20
|
}) => {
|
|
20
21
|
const getPayload = (event) => {
|
|
21
22
|
const payloadTemplate = (
|
|
@@ -28,7 +29,11 @@ export const createEventDispatchCallback = ({
|
|
|
28
29
|
if (typeof parseAndRenderFn !== "function") {
|
|
29
30
|
return payloadTemplate;
|
|
30
31
|
}
|
|
32
|
+
const payloadContext = typeof getPayloadContext === "function"
|
|
33
|
+
? getPayloadContext()
|
|
34
|
+
: {};
|
|
31
35
|
return parseAndRenderFn(payloadTemplate, {
|
|
36
|
+
...(payloadContext && typeof payloadContext === "object" ? payloadContext : {}),
|
|
32
37
|
_event: event,
|
|
33
38
|
});
|
|
34
39
|
};
|
|
@@ -162,6 +167,7 @@ export const createConfiguredEventListener = ({
|
|
|
162
167
|
stateKey,
|
|
163
168
|
fallbackCurrentTarget = null,
|
|
164
169
|
parseAndRenderFn,
|
|
170
|
+
getPayloadContext,
|
|
165
171
|
onMissingHandler,
|
|
166
172
|
nowFn = Date.now,
|
|
167
173
|
setTimeoutFn = setTimeout,
|
|
@@ -178,6 +184,7 @@ export const createConfiguredEventListener = ({
|
|
|
178
184
|
handlers,
|
|
179
185
|
onMissingHandler,
|
|
180
186
|
parseAndRenderFn,
|
|
187
|
+
getPayloadContext,
|
|
181
188
|
});
|
|
182
189
|
if (!callback) {
|
|
183
190
|
return null;
|
|
@@ -21,6 +21,7 @@ export const attachGlobalRefListeners = ({
|
|
|
21
21
|
document: globalThis.document,
|
|
22
22
|
},
|
|
23
23
|
parseAndRenderFn,
|
|
24
|
+
getPayloadContext,
|
|
24
25
|
timing = {
|
|
25
26
|
nowFn: Date.now,
|
|
26
27
|
setTimeoutFn: setTimeout,
|
|
@@ -54,6 +55,7 @@ export const attachGlobalRefListeners = ({
|
|
|
54
55
|
stateKey,
|
|
55
56
|
fallbackCurrentTarget: target,
|
|
56
57
|
parseAndRenderFn,
|
|
58
|
+
getPayloadContext,
|
|
57
59
|
nowFn: timing.nowFn,
|
|
58
60
|
setTimeoutFn: timing.setTimeoutFn,
|
|
59
61
|
clearTimeoutFn: timing.clearTimeoutFn,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const getDefaultFetch = () => {
|
|
2
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function") {
|
|
3
|
+
return globalThis.fetch.bind(globalThis);
|
|
4
|
+
}
|
|
5
|
+
return null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const normalizeAvailableLocales = (locales = []) => [...new Set(locales)];
|
|
9
|
+
|
|
10
|
+
const createUnknownLocaleError = ({ locale, availableLocales }) => {
|
|
11
|
+
return new Error(
|
|
12
|
+
`[i18n] Unknown locale "${locale}". Available locales: ${availableLocales.join(", ")}.`,
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const createI18nRuntime = ({
|
|
17
|
+
defaultLocale,
|
|
18
|
+
fallbackLocale = defaultLocale,
|
|
19
|
+
locales = [],
|
|
20
|
+
urls = {},
|
|
21
|
+
initialCatalogs = {},
|
|
22
|
+
fetchFn = getDefaultFetch(),
|
|
23
|
+
} = {}) => {
|
|
24
|
+
const availableLocales = normalizeAvailableLocales(locales);
|
|
25
|
+
const catalogs = new Map(Object.entries(initialCatalogs || {}));
|
|
26
|
+
const listeners = new Set();
|
|
27
|
+
let currentLocale = defaultLocale || fallbackLocale || availableLocales[0] || null;
|
|
28
|
+
let localeRequestVersion = 0;
|
|
29
|
+
|
|
30
|
+
const assertKnownLocale = (locale) => {
|
|
31
|
+
if (!availableLocales.includes(locale)) {
|
|
32
|
+
throw createUnknownLocaleError({ locale, availableLocales });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getMessages = () => {
|
|
37
|
+
return (
|
|
38
|
+
catalogs.get(currentLocale) ||
|
|
39
|
+
catalogs.get(fallbackLocale) ||
|
|
40
|
+
Object.create(null)
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const notify = () => {
|
|
45
|
+
listeners.forEach((listener) => {
|
|
46
|
+
listener(currentLocale);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const load = async (locale) => {
|
|
51
|
+
assertKnownLocale(locale);
|
|
52
|
+
|
|
53
|
+
if (catalogs.has(locale)) {
|
|
54
|
+
return catalogs.get(locale);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const url = urls[locale];
|
|
58
|
+
if (!url) {
|
|
59
|
+
throw new Error(`[i18n] Missing URL for locale "${locale}".`);
|
|
60
|
+
}
|
|
61
|
+
if (!fetchFn) {
|
|
62
|
+
throw new Error("[i18n] fetch is not available for lazy locale loading.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const response = await fetchFn(url);
|
|
66
|
+
if (!response?.ok) {
|
|
67
|
+
throw new Error(`[i18n] Failed to load locale "${locale}" from ${url}.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const catalog = await response.json();
|
|
71
|
+
catalogs.set(locale, catalog);
|
|
72
|
+
return catalog;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const activateLocale = (locale) => {
|
|
76
|
+
currentLocale = locale;
|
|
77
|
+
notify();
|
|
78
|
+
return getMessages();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const set = async (locale) => {
|
|
82
|
+
assertKnownLocale(locale);
|
|
83
|
+
const requestVersion = localeRequestVersion + 1;
|
|
84
|
+
localeRequestVersion = requestVersion;
|
|
85
|
+
const isLatestRequest = () => requestVersion === localeRequestVersion;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await load(locale);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!isLatestRequest()) {
|
|
91
|
+
return getMessages();
|
|
92
|
+
}
|
|
93
|
+
if (locale === fallbackLocale) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await load(fallbackLocale);
|
|
98
|
+
} catch (fallbackError) {
|
|
99
|
+
if (!isLatestRequest()) {
|
|
100
|
+
return getMessages();
|
|
101
|
+
}
|
|
102
|
+
throw fallbackError;
|
|
103
|
+
}
|
|
104
|
+
if (!isLatestRequest()) {
|
|
105
|
+
return getMessages();
|
|
106
|
+
}
|
|
107
|
+
return activateLocale(fallbackLocale);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isLatestRequest()) {
|
|
111
|
+
return getMessages();
|
|
112
|
+
}
|
|
113
|
+
return activateLocale(locale);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const ready = async () => {
|
|
117
|
+
if (!currentLocale) {
|
|
118
|
+
return getMessages();
|
|
119
|
+
}
|
|
120
|
+
if (catalogs.has(currentLocale)) {
|
|
121
|
+
return getMessages();
|
|
122
|
+
}
|
|
123
|
+
return set(currentLocale);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const current = () => currentLocale;
|
|
127
|
+
const available = () => [...availableLocales];
|
|
128
|
+
const subscribe = (listener) => {
|
|
129
|
+
listeners.add(listener);
|
|
130
|
+
return () => {
|
|
131
|
+
listeners.delete(listener);
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const localeService = {
|
|
136
|
+
available,
|
|
137
|
+
current,
|
|
138
|
+
load,
|
|
139
|
+
ready,
|
|
140
|
+
set,
|
|
141
|
+
subscribe,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
available,
|
|
146
|
+
current,
|
|
147
|
+
getMessages,
|
|
148
|
+
load,
|
|
149
|
+
locale: localeService,
|
|
150
|
+
localeService,
|
|
151
|
+
ready,
|
|
152
|
+
set,
|
|
153
|
+
subscribe,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -5,13 +5,26 @@ export const createRuntimeDeps = ({
|
|
|
5
5
|
store,
|
|
6
6
|
render,
|
|
7
7
|
}) => {
|
|
8
|
-
|
|
8
|
+
const runtimeDeps = {
|
|
9
9
|
...baseDeps,
|
|
10
10
|
refs,
|
|
11
11
|
dispatchEvent,
|
|
12
12
|
store,
|
|
13
13
|
render,
|
|
14
14
|
};
|
|
15
|
+
|
|
16
|
+
if (baseDeps?.__rtglI18nRuntime) {
|
|
17
|
+
Object.defineProperty(runtimeDeps, "i18n", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
get() {
|
|
21
|
+
return baseDeps.__rtglI18nRuntime.getMessages();
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
runtimeDeps.locale = baseDeps.__rtglI18nRuntime.locale;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return runtimeDeps;
|
|
15
28
|
};
|
|
16
29
|
|
|
17
30
|
export const createStoreActionDispatcher = ({
|