@itrocks/framework 0.6.3 → 0.6.4
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 +18 -0
- package/cjs/dependencies.js +8 -8
- package/cjs/language.d.ts +5 -0
- package/cjs/language.js +90 -0
- package/cjs/main.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,24 @@ import { beforeFrameworkRun } from '@itrocks/framework'
|
|
|
74
74
|
beforeFrameworkRun(() => import('./src/dependencies'))
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
### Request languages
|
|
78
|
+
|
|
79
|
+
The framework can select one translation catalog for each request without sharing language state between concurrent
|
|
80
|
+
requests. Configure the supported languages and the source language in the application configuration:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
translate:
|
|
84
|
+
defaultLanguage: fr-FR
|
|
85
|
+
sourceLanguage: en-US
|
|
86
|
+
supportedLanguages:
|
|
87
|
+
- en-US
|
|
88
|
+
- fr-FR
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The language stored in `request.session.language` takes precedence. Otherwise, the framework negotiates the
|
|
92
|
+
`Accept-Language` header, matching an exact locale first and then its base language. Unsupported values fall back to
|
|
93
|
+
`defaultLanguage`. Every response includes `Content-Language` and varies on `Accept-Language`.
|
|
94
|
+
|
|
77
95
|
### Using framework reflection helpers
|
|
78
96
|
|
|
79
97
|
`ReflectClass` and `ReflectProperty` behave like their counterparts from
|
package/cjs/dependencies.js
CHANGED
|
@@ -45,9 +45,11 @@ const transformer_1 = require("@itrocks/transformer");
|
|
|
45
45
|
const transformer_2 = require("@itrocks/transformer");
|
|
46
46
|
const transformer_3 = require("@itrocks/transformer");
|
|
47
47
|
const translate_1 = require("@itrocks/translate");
|
|
48
|
+
const translate_2 = require("@itrocks/translate");
|
|
48
49
|
const date_fns_1 = require("date-fns");
|
|
49
50
|
const node_path_1 = require("node:path");
|
|
50
51
|
const node_path_2 = require("node:path");
|
|
52
|
+
const language_1 = require("./language");
|
|
51
53
|
const sql_join_dependencies_1 = require("./sql-join-dependencies");
|
|
52
54
|
const sql_join_dependencies_2 = require("./sql-join-dependencies");
|
|
53
55
|
const sql_join_dependencies_3 = require("./sql-join-dependencies");
|
|
@@ -60,7 +62,7 @@ async function propertyOutput(object, property) {
|
|
|
60
62
|
return (0, transformer_1.applyTransformer)(await object[property], object, property, transformer_2.HTML, transformer_3.OUTPUT);
|
|
61
63
|
}
|
|
62
64
|
function bind() {
|
|
63
|
-
(0, class_view_1.classViewDependsOn)({ propertyOutput, requiredOf: required_1.requiredOf, tr:
|
|
65
|
+
(0, class_view_1.classViewDependsOn)({ propertyOutput, requiredOf: required_1.requiredOf, tr: translate_2.tr });
|
|
64
66
|
if (config_1.config.dataSource) {
|
|
65
67
|
(0, storage_1.createDataSource)(config_1.config.dataSource);
|
|
66
68
|
}
|
|
@@ -69,13 +71,13 @@ function bind() {
|
|
|
69
71
|
displayOf: property_view_1.displayOf,
|
|
70
72
|
fieldIdOf: rename_2.toCssId,
|
|
71
73
|
fieldNameOf: rename_2.toField,
|
|
72
|
-
formatDate: date => (0, date_fns_1.format)(date, (0,
|
|
74
|
+
formatDate: date => (0, date_fns_1.format)(date, (0, translate_2.tr)('dd/MM/yyyy', { ucFirst: false })),
|
|
73
75
|
ignoreTransformedValue: transformer_2.IGNORE,
|
|
74
|
-
parseDate: date => (0, date_fns_1.parse)(date, (0,
|
|
76
|
+
parseDate: date => (0, date_fns_1.parse)(date, (0, translate_2.tr)('dd/MM/yyyy', { ucFirst: false }), new Date),
|
|
75
77
|
propertyOutput,
|
|
76
78
|
representativeValueOf: class_view_2.representativeValueOf,
|
|
77
79
|
routeOf: route_2.routeOf,
|
|
78
|
-
tr:
|
|
80
|
+
tr: translate_2.tr
|
|
79
81
|
});
|
|
80
82
|
(0, file_1.initFileTransformers)();
|
|
81
83
|
(0, list_properties_1.initListProperties)();
|
|
@@ -141,10 +143,7 @@ function bind() {
|
|
|
141
143
|
? (await require('@itrocks/class-view').representativeValueOf(data))
|
|
142
144
|
: '' + data
|
|
143
145
|
});
|
|
144
|
-
(0,
|
|
145
|
-
(0, translate_1.trLoad)((0, node_path_1.join)(__dirname, '..', 'fr-FR.csv')).catch();
|
|
146
|
-
(0, translate_1.trLoad)((0, node_path_1.join)(app_dir_1.appDir, 'app', 'fr-FR.csv')).catch();
|
|
147
|
-
(0, translate_1.trLoad)((0, node_path_1.join)(app_dir_1.appDir, 'fr-FR.csv')).catch();
|
|
146
|
+
(0, language_1.configureLanguages)();
|
|
148
147
|
action_1.Action.prototype.htmlTemplateResponse = async function (data, request, templateFile, statusCode = 200, headers = {}) {
|
|
149
148
|
const containerData = Object.assign(defaultContainerData(request), this);
|
|
150
149
|
const contained = !request.request.headers['xhr-info'] && config_1.config.container?.file;
|
|
@@ -171,6 +170,7 @@ function defaultContainerData(request) {
|
|
|
171
170
|
return Object.assign({
|
|
172
171
|
action: request.action,
|
|
173
172
|
favicon: config_1.config.container?.favicon ?? (0, node_path_2.normalize)((0, node_path_1.join)(__dirname, '../favicon.ico')),
|
|
173
|
+
language: (0, translate_1.lang)(),
|
|
174
174
|
manifest: config_1.config.container?.manifest ? [config_1.config.container.manifest] : [],
|
|
175
175
|
request,
|
|
176
176
|
scripts: config_1.config.container?.scripts,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Request } from '@itrocks/request-response';
|
|
2
|
+
export declare function configureLanguages(): void;
|
|
3
|
+
export declare function languageFromHeader(header: string | undefined, supportedLanguages?: string[], defaultLanguage?: string): string;
|
|
4
|
+
export declare function languageOf(request: Request): string;
|
|
5
|
+
export declare function withRequestLanguage<T>(request: Request, callback: () => T | Promise<T>): Promise<T>;
|
package/cjs/language.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureLanguages = configureLanguages;
|
|
4
|
+
exports.languageFromHeader = languageFromHeader;
|
|
5
|
+
exports.languageOf = languageOf;
|
|
6
|
+
exports.withRequestLanguage = withRequestLanguage;
|
|
7
|
+
const app_dir_1 = require("@itrocks/app-dir");
|
|
8
|
+
const config_1 = require("@itrocks/config");
|
|
9
|
+
const translate_1 = require("@itrocks/translate");
|
|
10
|
+
const translate_2 = require("@itrocks/translate");
|
|
11
|
+
const translate_3 = require("@itrocks/translate");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
let configuration = {
|
|
14
|
+
defaultLanguage: 'fr-FR',
|
|
15
|
+
sourceLanguage: 'en-US',
|
|
16
|
+
supportedLanguages: ['fr-FR']
|
|
17
|
+
};
|
|
18
|
+
let loading = Promise.resolve(undefined);
|
|
19
|
+
function canonicalLanguage(value) {
|
|
20
|
+
try {
|
|
21
|
+
return Intl.getCanonicalLocales(value)[0];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function configureLanguages() {
|
|
28
|
+
const options = (config_1.config.translate ?? {});
|
|
29
|
+
const defaultLanguage = canonicalLanguage(options.defaultLanguage) ?? 'fr-FR';
|
|
30
|
+
const sourceLanguage = canonicalLanguage(options.sourceLanguage) ?? 'en-US';
|
|
31
|
+
const supportedLanguages = options.supportedLanguages
|
|
32
|
+
.map(language => canonicalLanguage(language))
|
|
33
|
+
.filter((language) => !!language);
|
|
34
|
+
if (!supportedLanguages.includes(defaultLanguage)) {
|
|
35
|
+
supportedLanguages.push(defaultLanguage);
|
|
36
|
+
}
|
|
37
|
+
configuration = { defaultLanguage, sourceLanguage, supportedLanguages: [...new Set(supportedLanguages)] };
|
|
38
|
+
(0, translate_1.trInit)(defaultLanguage);
|
|
39
|
+
loading = (async () => {
|
|
40
|
+
// TODO search language files everywhere in application modules, not only into app
|
|
41
|
+
const directories = [(0, node_path_1.join)(__dirname, '..'), (0, node_path_1.join)(app_dir_1.appDir, 'app'), app_dir_1.appDir];
|
|
42
|
+
for (const language of configuration.supportedLanguages) {
|
|
43
|
+
for (const directory of directories) {
|
|
44
|
+
const file = (0, node_path_1.join)(directory, language + '.csv');
|
|
45
|
+
await (0, translate_2.trLoad)(file, { language });
|
|
46
|
+
if ((language !== sourceLanguage) && configuration.supportedLanguages.includes(sourceLanguage)) {
|
|
47
|
+
await (0, translate_2.trLoad)(file, { language: sourceLanguage, reverse: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
}
|
|
53
|
+
function languageFromHeader(header, supportedLanguages = configuration.supportedLanguages, defaultLanguage = configuration.defaultLanguage) {
|
|
54
|
+
const preferences = (header ?? '').split(',').map((item, index) => {
|
|
55
|
+
const [range, ...parameters] = item.trim().split(';');
|
|
56
|
+
const qualityParameter = parameters.find(parameter => parameter.trim().startsWith('q='));
|
|
57
|
+
const quality = qualityParameter ? Number(qualityParameter.trim().slice(2)) : 1;
|
|
58
|
+
return { index, quality: Number.isFinite(quality) ? quality : 0, range };
|
|
59
|
+
}).filter(preference => preference.range && (preference.quality > 0))
|
|
60
|
+
.sort((left, right) => right.quality - left.quality || left.index - right.index);
|
|
61
|
+
for (const preference of preferences) {
|
|
62
|
+
if (preference.range === '*')
|
|
63
|
+
return defaultLanguage;
|
|
64
|
+
const requested = canonicalLanguage(preference.range);
|
|
65
|
+
if (!requested)
|
|
66
|
+
continue;
|
|
67
|
+
const exact = supportedLanguages.find(language => language.toLowerCase() === requested.toLowerCase());
|
|
68
|
+
if (exact)
|
|
69
|
+
return exact;
|
|
70
|
+
const base = requested.split('-')[0].toLowerCase();
|
|
71
|
+
const compatible = supportedLanguages.find(language => language.split('-')[0].toLowerCase() === base);
|
|
72
|
+
if (compatible)
|
|
73
|
+
return compatible;
|
|
74
|
+
}
|
|
75
|
+
return defaultLanguage;
|
|
76
|
+
}
|
|
77
|
+
function languageOf(request) {
|
|
78
|
+
const preferred = canonicalLanguage(request.session.language);
|
|
79
|
+
if (preferred) {
|
|
80
|
+
const supported = configuration.supportedLanguages.find(language => language.toLowerCase() === preferred.toLowerCase());
|
|
81
|
+
if (supported)
|
|
82
|
+
return supported;
|
|
83
|
+
}
|
|
84
|
+
return languageFromHeader(request.headers['accept-language']);
|
|
85
|
+
}
|
|
86
|
+
async function withRequestLanguage(request, callback) {
|
|
87
|
+
await loading;
|
|
88
|
+
return (0, translate_3.trWithLanguage)(languageOf(request), callback);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=language.js.map
|
package/cjs/main.js
CHANGED
|
@@ -15,12 +15,27 @@ const route_1 = require("@itrocks/route");
|
|
|
15
15
|
const session_1 = require("@itrocks/session");
|
|
16
16
|
const store_1 = require("@itrocks/store");
|
|
17
17
|
const template_1 = require("@itrocks/template");
|
|
18
|
+
const translate_1 = require("@itrocks/translate");
|
|
18
19
|
const node_crypto_1 = require("node:crypto");
|
|
19
20
|
const node_path_1 = require("node:path");
|
|
20
21
|
const node_path_2 = require("node:path");
|
|
22
|
+
const language_1 = require("./language");
|
|
21
23
|
const servers_1 = require("./servers");
|
|
22
24
|
const ephemeralSessionSecret = (0, node_crypto_1.randomBytes)(32).toString('base64url');
|
|
23
25
|
template_1.frontScripts.push('/lib/air-datepicker/locale/en.js', '/lib/air-datepicker/locale/fr.js');
|
|
26
|
+
function appendVary(headers, value) {
|
|
27
|
+
const name = Object.keys(headers).find(name => (name.toLowerCase() === 'vary')) ?? 'Vary';
|
|
28
|
+
const values = (headers[name] ?? '').split(',').map(value => value.trim()).filter(Boolean);
|
|
29
|
+
if (!values.some(current => (current.toLowerCase() === value.toLowerCase()))) {
|
|
30
|
+
values.push(value);
|
|
31
|
+
}
|
|
32
|
+
headers[name] = values.join(', ');
|
|
33
|
+
}
|
|
34
|
+
function defaultHeader(headers, name, value) {
|
|
35
|
+
if (!Object.keys(headers).some(current => (current.toLowerCase() === name.toLowerCase()))) {
|
|
36
|
+
headers[name] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
24
39
|
async function execute(request) {
|
|
25
40
|
// Access control
|
|
26
41
|
if ((config_1.config.access?.free !== '*')
|
|
@@ -69,7 +84,12 @@ async function run() {
|
|
|
69
84
|
const server = new fastify_1.FastifyServer({
|
|
70
85
|
assetPath: app_dir_1.appDir,
|
|
71
86
|
cookie: config_1.config.session.cookie,
|
|
72
|
-
execute: request =>
|
|
87
|
+
execute: request => (0, language_1.withRequestLanguage)(request, async () => {
|
|
88
|
+
const response = await execute(new action_request_2.Request(request));
|
|
89
|
+
appendVary(response.headers, 'Accept-Language');
|
|
90
|
+
defaultHeader(response.headers, 'Content-Language', (0, translate_1.lang)());
|
|
91
|
+
return response;
|
|
92
|
+
}),
|
|
73
93
|
favicon: config_1.config.container?.favicon ?? (0, node_path_2.normalize)((0, node_path_1.join)(__dirname, '../favicon.png')),
|
|
74
94
|
fileSize: config_1.config.server.fileSize,
|
|
75
95
|
frontScripts: template_1.frontScripts,
|
package/package.json
CHANGED
|
@@ -103,5 +103,5 @@
|
|
|
103
103
|
"build:front": "tsc -p src/front/tsconfig.json && node -e \"const fs=require('node:fs');for(const file of fs.readdirSync('.').filter(file=>file.endsWith('.js')||file.endsWith('.d.ts')))fs.writeFileSync(file,fs.readFileSync(file,'utf8').replaceAll('../../',''))\"",
|
|
104
104
|
"test": "npm run build && node --test test/*.test.js"
|
|
105
105
|
},
|
|
106
|
-
"version": "0.6.
|
|
106
|
+
"version": "0.6.4"
|
|
107
107
|
}
|