@itrocks/framework 0.6.2 → 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 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
@@ -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: translate_1.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, translate_1.tr)('dd/MM/yyyy', { ucFirst: false })),
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, translate_1.tr)('dd/MM/yyyy', { ucFirst: false }), new Date),
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: translate_1.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, translate_1.trInit)('fr-FR');
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>;
@@ -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,13 +84,21 @@ 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 => execute(new action_request_2.Request(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')),
94
+ fileSize: config_1.config.server.fileSize,
74
95
  frontScripts: template_1.frontScripts,
96
+ headers: config_1.config.server.headers,
75
97
  host: config_1.config.server.host,
76
98
  manifest: config_1.config.container?.manifest,
77
99
  port: config_1.config.server.port,
78
100
  rolling: config_1.config.session.rolling,
101
+ sameOrigin: config_1.config.server.sameOrigin,
79
102
  scriptCalls: ['loadCss', 'loadScript'],
80
103
  secret: config_1.config.session.secret ?? config_1.config.secret ?? ephemeralSessionSecret,
81
104
  secure: config_1.config.server.secure ?? 'auto',
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.2"
106
+ "version": "0.6.4"
107
107
  }