@itrocks/framework 0.6.3 → 0.6.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/README.md +22 -0
- package/cjs/dependencies.js +8 -8
- package/cjs/language.d.ts +5 -0
- package/cjs/language.js +85 -0
- package/cjs/main.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,28 @@ 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. Templates and translation keys are written in English. Configure the supported languages in the application
|
|
81
|
+
configuration:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
translate:
|
|
85
|
+
defaultLanguage: fr-FR
|
|
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`. The framework loads each `<language>.csv` catalog directly; English needs no catalog because it is
|
|
94
|
+
the source language. Every response includes `Content-Language` and varies on `Accept-Language`.
|
|
95
|
+
|
|
96
|
+
Catalog rows contain lowercase, context-free fragments. Sentence periods stay in templates so the translation engine
|
|
97
|
+
can translate each sentence independently and restore contextual capitalization.
|
|
98
|
+
|
|
77
99
|
### Using framework reflection helpers
|
|
78
100
|
|
|
79
101
|
`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,85 @@
|
|
|
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
|
+
supportedLanguages: ['fr-FR']
|
|
16
|
+
};
|
|
17
|
+
let loading = Promise.resolve(undefined);
|
|
18
|
+
function canonicalLanguage(value) {
|
|
19
|
+
try {
|
|
20
|
+
return Intl.getCanonicalLocales(value)[0];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function configureLanguages() {
|
|
27
|
+
const options = (config_1.config.translate ?? {});
|
|
28
|
+
const defaultLanguage = canonicalLanguage(options.defaultLanguage) ?? 'fr-FR';
|
|
29
|
+
const supportedLanguages = options.supportedLanguages
|
|
30
|
+
.map(language => canonicalLanguage(language))
|
|
31
|
+
.filter((language) => !!language);
|
|
32
|
+
if (!supportedLanguages.includes(defaultLanguage)) {
|
|
33
|
+
supportedLanguages.push(defaultLanguage);
|
|
34
|
+
}
|
|
35
|
+
configuration = { defaultLanguage, supportedLanguages: [...new Set(supportedLanguages)] };
|
|
36
|
+
(0, translate_1.trInit)(defaultLanguage);
|
|
37
|
+
loading = (async () => {
|
|
38
|
+
// TODO search language files everywhere in application modules, not only into app
|
|
39
|
+
const directories = [(0, node_path_1.join)(__dirname, '..'), (0, node_path_1.join)(app_dir_1.appDir, 'app'), app_dir_1.appDir];
|
|
40
|
+
for (const language of configuration.supportedLanguages) {
|
|
41
|
+
for (const directory of directories) {
|
|
42
|
+
const file = (0, node_path_1.join)(directory, language + '.csv');
|
|
43
|
+
await (0, translate_2.trLoad)(file, language);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
}
|
|
48
|
+
function languageFromHeader(header, supportedLanguages = configuration.supportedLanguages, defaultLanguage = configuration.defaultLanguage) {
|
|
49
|
+
const preferences = (header ?? '').split(',').map((item, index) => {
|
|
50
|
+
const [range, ...parameters] = item.trim().split(';');
|
|
51
|
+
const qualityParameter = parameters.find(parameter => parameter.trim().startsWith('q='));
|
|
52
|
+
const quality = qualityParameter ? Number(qualityParameter.trim().slice(2)) : 1;
|
|
53
|
+
return { index, quality: Number.isFinite(quality) ? quality : 0, range };
|
|
54
|
+
}).filter(preference => preference.range && (preference.quality > 0))
|
|
55
|
+
.sort((left, right) => right.quality - left.quality || left.index - right.index);
|
|
56
|
+
for (const preference of preferences) {
|
|
57
|
+
if (preference.range === '*')
|
|
58
|
+
return defaultLanguage;
|
|
59
|
+
const requested = canonicalLanguage(preference.range);
|
|
60
|
+
if (!requested)
|
|
61
|
+
continue;
|
|
62
|
+
const exact = supportedLanguages.find(language => language.toLowerCase() === requested.toLowerCase());
|
|
63
|
+
if (exact)
|
|
64
|
+
return exact;
|
|
65
|
+
const base = requested.split('-')[0].toLowerCase();
|
|
66
|
+
const compatible = supportedLanguages.find(language => language.split('-')[0].toLowerCase() === base);
|
|
67
|
+
if (compatible)
|
|
68
|
+
return compatible;
|
|
69
|
+
}
|
|
70
|
+
return defaultLanguage;
|
|
71
|
+
}
|
|
72
|
+
function languageOf(request) {
|
|
73
|
+
const preferred = canonicalLanguage(request.session.language);
|
|
74
|
+
if (preferred) {
|
|
75
|
+
const supported = configuration.supportedLanguages.find(language => language.toLowerCase() === preferred.toLowerCase());
|
|
76
|
+
if (supported)
|
|
77
|
+
return supported;
|
|
78
|
+
}
|
|
79
|
+
return languageFromHeader(request.headers['accept-language']);
|
|
80
|
+
}
|
|
81
|
+
async function withRequestLanguage(request, callback) {
|
|
82
|
+
await loading;
|
|
83
|
+
return (0, translate_3.trWithLanguage)(languageOf(request), callback);
|
|
84
|
+
}
|
|
85
|
+
//# 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.5"
|
|
107
107
|
}
|