@companieshouse/ch-node-utils 0.1.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/LICENSE +21 -0
- package/README.md +2 -0
- package/lib/constants/constants.d.ts +1 -0
- package/lib/constants/constants.js +5 -0
- package/lib/constants/constants.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/middleware/manageLocales.middleware.d.ts +2 -0
- package/lib/middleware/manageLocales.middleware.js +37 -0
- package/lib/middleware/manageLocales.middleware.js.map +1 -0
- package/lib/services/locales/locales.service.d.ts +9 -0
- package/lib/services/locales/locales.service.js +23 -0
- package/lib/services/locales/locales.service.js.map +1 -0
- package/lib/utils/i18nCh.d.ts +13 -0
- package/lib/utils/i18nCh.js +113 -0
- package/lib/utils/i18nCh.js.map +1 -0
- package/lib/utils/languageNames.d.ts +14 -0
- package/lib/utils/languageNames.js +45 -0
- package/lib/utils/languageNames.js.map +1 -0
- package/lib/utils/subDirs.d.ts +3 -0
- package/lib/utils/subDirs.js +17 -0
- package/lib/utils/subDirs.js.map +1 -0
- package/package.json +54 -0
- package/templates/add-lang-to-url.njk +33 -0
- package/templates/locales-banner.njk +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Companies House
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const QUERY_PAR_LANG = "lang";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,cAAc,GAAG,MAAM,CAAA"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import SubDirs from "./utils/subDirs";
|
|
2
|
+
import LanguageNames, { NamedIsoCode } from "./utils/languageNames";
|
|
3
|
+
import i18nCh from "./utils/i18nCh";
|
|
4
|
+
import LocalesService from "./services/locales/locales.service";
|
|
5
|
+
import { LocalesMiddleware } from "./middleware/manageLocales.middleware";
|
|
6
|
+
import { QUERY_PAR_LANG } from "./constants/constants";
|
|
7
|
+
export { SubDirs, NamedIsoCode, LanguageNames, i18nCh, LocalesMiddleware, LocalesService, QUERY_PAR_LANG };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QUERY_PAR_LANG = exports.LocalesService = exports.LocalesMiddleware = exports.i18nCh = exports.LanguageNames = exports.NamedIsoCode = exports.SubDirs = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const subDirs_1 = tslib_1.__importDefault(require("./utils/subDirs"));
|
|
6
|
+
exports.SubDirs = subDirs_1.default;
|
|
7
|
+
const languageNames_1 = tslib_1.__importStar(require("./utils/languageNames"));
|
|
8
|
+
exports.LanguageNames = languageNames_1.default;
|
|
9
|
+
Object.defineProperty(exports, "NamedIsoCode", { enumerable: true, get: function () { return languageNames_1.NamedIsoCode; } });
|
|
10
|
+
const i18nCh_1 = tslib_1.__importDefault(require("./utils/i18nCh"));
|
|
11
|
+
exports.i18nCh = i18nCh_1.default;
|
|
12
|
+
const locales_service_1 = tslib_1.__importDefault(require("./services/locales/locales.service"));
|
|
13
|
+
exports.LocalesService = locales_service_1.default;
|
|
14
|
+
const manageLocales_middleware_1 = require("./middleware/manageLocales.middleware");
|
|
15
|
+
Object.defineProperty(exports, "LocalesMiddleware", { enumerable: true, get: function () { return manageLocales_middleware_1.LocalesMiddleware; } });
|
|
16
|
+
const constants_1 = require("./constants/constants");
|
|
17
|
+
Object.defineProperty(exports, "QUERY_PAR_LANG", { enumerable: true, get: function () { return constants_1.QUERY_PAR_LANG; } });
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;AAAA,sEAAsC;AAQnC,kBARI,iBAAO,CAQJ;AAPV,+EAAkE;AAS/D,wBATI,uBAAa,CASJ;AADb,6FARoB,4BAAY,OAQpB;AAPf,oEAAoC;AASjC,iBATI,gBAAM,CASJ;AART,iGAA+D;AAU5D,yBAVI,yBAAc,CAUJ;AATjB,oFAAyE;AAQtE,kGARM,4CAAiB,OAQN;AAPpB,qDAAsD;AASnD,+FATM,0BAAc,OASN"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalesMiddleware = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const constants_1 = require("../constants/constants");
|
|
6
|
+
const locales_service_1 = tslib_1.__importDefault(require("../services/locales/locales.service"));
|
|
7
|
+
const languageNames_1 = tslib_1.__importDefault(require("../utils/languageNames"));
|
|
8
|
+
function LocalesMiddleware() {
|
|
9
|
+
return (req, res, next) => {
|
|
10
|
+
var _a;
|
|
11
|
+
console.log("----------X2 (ManageLocales) ------------");
|
|
12
|
+
let lang = (req.query[constants_1.QUERY_PAR_LANG] ||
|
|
13
|
+
req.body.lang ||
|
|
14
|
+
((_a = req.session) === null || _a === void 0 ? void 0 : _a.getExtraData(constants_1.QUERY_PAR_LANG)));
|
|
15
|
+
console.log(`=====1=====Mng Loc ....(init lang=${lang}) - body:${req.body.lang}`);
|
|
16
|
+
console.log("body:");
|
|
17
|
+
console.log(req.body);
|
|
18
|
+
console.log("=====2=====Mng Loc");
|
|
19
|
+
if (lang === undefined || !languageNames_1.default.isSupportedLocale(locales_service_1.default.getInstance().localesFolder, lang)) {
|
|
20
|
+
lang = "en";
|
|
21
|
+
}
|
|
22
|
+
if (req.session) {
|
|
23
|
+
req.session.setExtraData(constants_1.QUERY_PAR_LANG, lang);
|
|
24
|
+
}
|
|
25
|
+
req.lang = lang;
|
|
26
|
+
const [pathWithoutQuery] = req.url.split("?");
|
|
27
|
+
req.url = pathWithoutQuery;
|
|
28
|
+
const currentUrl = `${req.protocol}://${req.get("host")}${pathWithoutQuery}`;
|
|
29
|
+
res.locals.currentUrl = currentUrl;
|
|
30
|
+
res.locals.htmlLang = lang;
|
|
31
|
+
res.locals.pageTitleLang = lang;
|
|
32
|
+
res.locals.mainLang = lang;
|
|
33
|
+
return next();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
exports.LocalesMiddleware = LocalesMiddleware;
|
|
37
|
+
//# sourceMappingURL=manageLocales.middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manageLocales.middleware.js","sourceRoot":"","sources":["../../src/middleware/manageLocales.middleware.ts"],"names":[],"mappings":";;;;AACA,sDAAuD;AACvD,kGAAgE;AAEhE,mFAAoD;AAEpD,SAAgB,iBAAiB;IAC3B,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;;QACxD,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAA;QAE7D,IAAI,IAAI,GAAuB,CAAS,GAAG,CAAC,KAAK,CAAC,0BAAc,CAAC;YACjC,GAAG,CAAC,IAAI,CAAC,IAAI;aACb,MAAA,GAAG,CAAC,OAAO,0CAAE,YAAY,CAAS,0BAAc,CAAC,CAAA,CAAC,CAAA;QAClF,OAAO,CAAC,GAAG,CAAC,qCAAqC,IAAI,YAAY,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;QAGjF,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACpB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAGrB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QAClC,IAAI,IAAI,KAAK,SAAS,IAAI,CAAE,uBAAa,CAAC,iBAAiB,CAAE,yBAAc,CAAC,WAAW,EAAE,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE;YAC1G,IAAI,GAAG,IAAI,CAAA;SACf;QACD,IAAI,GAAG,CAAC,OAAO,EAAE;YACf,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,0BAAc,EAAE,IAAI,CAAC,CAAA;SAC/C;QACD,GAAG,CAAC,IAAI,GAAG,IAAI,CAAA;QACf,MAAM,CAAC,gBAAgB,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC7C,GAAG,CAAC,GAAG,GAAG,gBAAgB,CAAA;QAE1B,MAAM,UAAU,GAAG,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,gBAAgB,EAAE,CAAA;QAC5E,GAAG,CAAC,MAAM,CAAC,UAAU,GAAG,UAAU,CAAA;QAGlC,GAAG,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAA;QAC1B,GAAG,CAAC,MAAM,CAAC,aAAa,GAAG,IAAI,CAAA;QAC/B,GAAG,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAA;QAE1B,OAAO,IAAI,EAAE,CAAA;IACf,CAAC,CAAA;AACH,CAAC;AAnCD,8CAmCC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import i18nCh from "../../utils/i18nCh";
|
|
2
|
+
export default class LocalesService {
|
|
3
|
+
private static instance;
|
|
4
|
+
enabled: Boolean;
|
|
5
|
+
localesFolder: string;
|
|
6
|
+
i18nCh: i18nCh;
|
|
7
|
+
private constructor();
|
|
8
|
+
static getInstance(localesFolder?: string, enabled?: Boolean): LocalesService;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const i18nCh_1 = tslib_1.__importDefault(require("../../utils/i18nCh"));
|
|
5
|
+
class LocalesService {
|
|
6
|
+
constructor(localesFolder, enabled) {
|
|
7
|
+
console.log("----------X0 (construct) ------------");
|
|
8
|
+
this.enabled = enabled;
|
|
9
|
+
this.localesFolder = localesFolder;
|
|
10
|
+
this.i18nCh = i18nCh_1.default.getInstance(localesFolder);
|
|
11
|
+
console.log(`enabled: ${this.enabled} / path: ${this.localesFolder}`);
|
|
12
|
+
console.log(`enabled: ${this.enabled} / path: ${this.localesFolder} / i18nCh: ${this.i18nCh}`);
|
|
13
|
+
}
|
|
14
|
+
static getInstance(localesFolder = "locales", enabled = false) {
|
|
15
|
+
console.log("----------X0 (getInst) ------------");
|
|
16
|
+
if (!LocalesService.instance) {
|
|
17
|
+
LocalesService.instance = new LocalesService(localesFolder, enabled);
|
|
18
|
+
}
|
|
19
|
+
return LocalesService.instance;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.default = LocalesService;
|
|
23
|
+
//# sourceMappingURL=locales.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locales.service.js","sourceRoot":"","sources":["../../../src/services/locales/locales.service.ts"],"names":[],"mappings":";;;AAAA,wEAAwC;AAExC,MAAqB,cAAc;IAOhC,YAAoB,aAAqB,EAAE,OAAgB;QACxD,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAA;QAEpD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAA;QAClC,IAAI,CAAC,MAAM,GAAG,gBAAM,CAAC,WAAW,CAAC,aAAa,CAAC,CAAA;QAC/C,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,OAAO,YAAY,IAAI,CAAC,aAAa,EAAE,CAAC,CAAA;QACrE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,OAAO,YAAY,IAAI,CAAC,aAAa,cAAc,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IACjG,CAAC;IAIM,MAAM,CAAC,WAAW,CAAC,gBAAwB,SAAS,EAAE,UAAmB,KAAK;QAClF,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAA;QAClD,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE;YAC3B,cAAc,CAAC,QAAQ,GAAG,IAAI,cAAc,CAAC,aAAa,EAAE,OAAO,CAAC,CAAA;SACtE;QACD,OAAO,cAAc,CAAC,QAAQ,CAAC;IAClC,CAAC;CACH;AA1BD,iCA0BC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default class i18nCh {
|
|
2
|
+
private static instance;
|
|
3
|
+
private i18nInst;
|
|
4
|
+
private localesFolder;
|
|
5
|
+
private nameSpaces;
|
|
6
|
+
private constructor();
|
|
7
|
+
static getInstance(localesFolder?: string, nameSpaces?: string[], lang?: string): i18nCh;
|
|
8
|
+
private loadAllNamespaces;
|
|
9
|
+
private changeLanguage;
|
|
10
|
+
resolveNamespacesKeys(lang: string, vars?: any): any;
|
|
11
|
+
resolveSingleKey(key: string, lang: string, vars?: any): string;
|
|
12
|
+
loadNamespaces(nameSpaces?: string[]): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const i18next_1 = tslib_1.__importDefault(require("i18next"));
|
|
5
|
+
const i18next_fs_backend_1 = tslib_1.__importDefault(require("i18next-fs-backend"));
|
|
6
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
7
|
+
const fs_1 = tslib_1.__importStar(require("fs"));
|
|
8
|
+
class i18nCh {
|
|
9
|
+
constructor(localesFolder = "", nameSpaces = [], lang = "en") {
|
|
10
|
+
this.nameSpaces = [];
|
|
11
|
+
try {
|
|
12
|
+
if (!localesFolder) {
|
|
13
|
+
throw new Error("i18nCh initialization error: path to locales must be provided");
|
|
14
|
+
}
|
|
15
|
+
this.localesFolder = localesFolder;
|
|
16
|
+
if (nameSpaces.length === 0) {
|
|
17
|
+
nameSpaces = this.loadAllNamespaces();
|
|
18
|
+
}
|
|
19
|
+
this.nameSpaces = nameSpaces;
|
|
20
|
+
this.i18nInst = i18next_1.default;
|
|
21
|
+
this.i18nInst
|
|
22
|
+
.use(i18next_fs_backend_1.default)
|
|
23
|
+
.init({
|
|
24
|
+
initImmediate: false,
|
|
25
|
+
ns: nameSpaces,
|
|
26
|
+
partialBundledLanguages: true,
|
|
27
|
+
lng: lang,
|
|
28
|
+
fallbackLng: "en",
|
|
29
|
+
preload: (0, fs_1.readdirSync)(localesFolder).filter((fileName) => {
|
|
30
|
+
const joinedPath = path_1.default.join(localesFolder, fileName);
|
|
31
|
+
return (0, fs_1.lstatSync)(joinedPath).isDirectory();
|
|
32
|
+
}),
|
|
33
|
+
backend: {
|
|
34
|
+
loadPath: path_1.default.join(localesFolder, `{{lng}}/{{ns}}.json`)
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
static getInstance(localesFolder, nameSpaces, lang) {
|
|
43
|
+
if (!i18nCh.instance) {
|
|
44
|
+
i18nCh.instance = new i18nCh(localesFolder, nameSpaces, lang);
|
|
45
|
+
}
|
|
46
|
+
return i18nCh.instance;
|
|
47
|
+
}
|
|
48
|
+
loadAllNamespaces() {
|
|
49
|
+
const jsonFiles = [];
|
|
50
|
+
if (this.localesFolder) {
|
|
51
|
+
fs_1.default.readdirSync(path_1.default.join(this.localesFolder, "en")).forEach((file) => {
|
|
52
|
+
if (path_1.default.extname(file) === ".json") {
|
|
53
|
+
jsonFiles.push(path_1.default.basename(file, ".json"));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return jsonFiles;
|
|
58
|
+
}
|
|
59
|
+
changeLanguage(lang) {
|
|
60
|
+
this.i18nInst.changeLanguage(lang, (err) => {
|
|
61
|
+
if (err)
|
|
62
|
+
throw new Error(`i18nCh changeLanguage error: ${err}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
resolveNamespacesKeys(lang, vars = {}) {
|
|
66
|
+
let data = {};
|
|
67
|
+
try {
|
|
68
|
+
if (this.i18nInst && this.nameSpaces) {
|
|
69
|
+
this.changeLanguage(lang);
|
|
70
|
+
const keysValuesList = this.i18nInst.getDataByLanguage("en");
|
|
71
|
+
if (keysValuesList !== undefined) {
|
|
72
|
+
for (const [ns, value] of Object.entries(keysValuesList)) {
|
|
73
|
+
console.log(`${ns}: ${this.nameSpaces}`);
|
|
74
|
+
if (this.nameSpaces.includes(ns)) {
|
|
75
|
+
console.log(`${ns}`);
|
|
76
|
+
for (const [key] of Object.entries(value)) {
|
|
77
|
+
console.log(`${key}`);
|
|
78
|
+
data[key] = this.i18nInst.t(key, { lng: lang, ns: ns, vars });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
resolveSingleKey(key, lang, vars = {}) {
|
|
91
|
+
let t = key;
|
|
92
|
+
if (this.i18nInst) {
|
|
93
|
+
try {
|
|
94
|
+
this.changeLanguage(lang);
|
|
95
|
+
t = this.i18nInst.t(key, { lng: lang, ns: this.nameSpaces, vars });
|
|
96
|
+
console.log(`searched for key:${key} lang:${lang} and got: ${t}`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return t;
|
|
103
|
+
}
|
|
104
|
+
loadNamespaces(nameSpaces = []) {
|
|
105
|
+
if (nameSpaces.length > 0 && this.i18nInst) {
|
|
106
|
+
this.i18nInst.loadNamespaces(nameSpaces, (err) => {
|
|
107
|
+
throw new Error(`i18nCh loadNamespaces error - unable to load namespaces: ${nameSpaces} - ${err}`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.default = i18nCh;
|
|
113
|
+
//# sourceMappingURL=i18nCh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18nCh.js","sourceRoot":"","sources":["../../src/utils/i18nCh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class NamedIsoCode {
|
|
2
|
+
IsoCode: string;
|
|
3
|
+
Name: string;
|
|
4
|
+
constructor(IsoCode: string, Name: string);
|
|
5
|
+
}
|
|
6
|
+
export default class LanguageNames {
|
|
7
|
+
static getNamesArray(isoCodes: string[]): string[];
|
|
8
|
+
static getNativeNamesArray(isoCodes: string[]): string[];
|
|
9
|
+
static getNamesObjectArray(isoCodes: string[]): NamedIsoCode[];
|
|
10
|
+
static getNativeNamesObjectArray(isoCodes: string[]): NamedIsoCode[];
|
|
11
|
+
private static _customSort;
|
|
12
|
+
static sourceLocales(localesDir: string): NamedIsoCode[];
|
|
13
|
+
static isSupportedLocale(localesDir: string, locale: string): boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NamedIsoCode = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const iso_639_1_1 = tslib_1.__importDefault(require("iso-639-1"));
|
|
6
|
+
const subDirs_js_1 = tslib_1.__importDefault(require("./subDirs.js"));
|
|
7
|
+
class NamedIsoCode {
|
|
8
|
+
constructor(IsoCode, Name) {
|
|
9
|
+
this.IsoCode = IsoCode;
|
|
10
|
+
this.Name = Name;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.NamedIsoCode = NamedIsoCode;
|
|
14
|
+
class LanguageNames {
|
|
15
|
+
static getNamesArray(isoCodes) {
|
|
16
|
+
return isoCodes.map((isoCode) => iso_639_1_1.default.getName(isoCode));
|
|
17
|
+
}
|
|
18
|
+
static getNativeNamesArray(isoCodes) {
|
|
19
|
+
return isoCodes.map((isoCode) => iso_639_1_1.default.getNativeName(isoCode));
|
|
20
|
+
}
|
|
21
|
+
static getNamesObjectArray(isoCodes) {
|
|
22
|
+
return isoCodes.map((isoCode) => (new NamedIsoCode(isoCode, iso_639_1_1.default.getName(isoCode))));
|
|
23
|
+
}
|
|
24
|
+
static getNativeNamesObjectArray(isoCodes) {
|
|
25
|
+
return isoCodes.map((isoCode) => (new NamedIsoCode(isoCode, iso_639_1_1.default.getNativeName(isoCode))));
|
|
26
|
+
}
|
|
27
|
+
static _customSort(a, b) {
|
|
28
|
+
if (a === "en") {
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
if (b === "en") {
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
return a.localeCompare(b);
|
|
35
|
+
}
|
|
36
|
+
static sourceLocales(localesDir) {
|
|
37
|
+
const localesArray = subDirs_js_1.default.getSubDirs(localesDir);
|
|
38
|
+
return LanguageNames.getNativeNamesObjectArray(localesArray.sort(LanguageNames._customSort));
|
|
39
|
+
}
|
|
40
|
+
static isSupportedLocale(localesDir, locale) {
|
|
41
|
+
return subDirs_js_1.default.getSubDirs(localesDir).includes(locale);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.default = LanguageNames;
|
|
45
|
+
//# sourceMappingURL=languageNames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"languageNames.js","sourceRoot":"","sources":["../../src/utils/languageNames.ts"],"names":[],"mappings":";;;;AAAA,kEAAgC;AAChC,sEAAmC;AAEnC,MAAa,YAAY;IACtB,YAAmB,OAAe,EAAS,IAAY;QAApC,YAAO,GAAP,OAAO,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;IACvD,CAAC;CACH;AAHD,oCAGC;AAED,MAAqB,aAAa;IAC/B,MAAM,CAAC,aAAa,CAAC,QAAkB;QACpC,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,mBAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,CAAC,mBAAmB,CAAC,QAAkB;QAC1C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,mBAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,CAAC,mBAAmB,CAAC,QAAkB;QAE1C,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,mBAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC;IAC5F,CAAC;IAED,MAAM,CAAC,yBAAyB,CAAC,QAAkB;QAEhD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,mBAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC;IAClG,CAAC;IAKO,MAAM,CAAC,WAAW,CAAC,CAAS,EAAE,CAAS;QAC/C,IAAI,CAAC,KAAK,IAAI,EAAE;YAAE,OAAO,CAAC,CAAC,CAAC;SAAE;QAC9B,IAAI,CAAC,KAAK,IAAI,EAAE;YAAE,OAAQ,CAAC,CAAC;SAAE;QAE9B,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,aAAa,CAAE,UAAkB;QACrC,MAAO,YAAY,GAAG,oBAAO,CAAC,UAAU,CAAE,UAAU,CAAC,CAAC;QACtD,OAAO,aAAa,CAAC,yBAAyB,CAAE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IACjG,CAAC;IAED,MAAM,CAAC,iBAAiB,CAAE,UAAkB,EAAE,MAAc;QACzD,OAAO,oBAAO,CAAC,UAAU,CAAE,UAAU,CAAC,CAAC,QAAQ,CAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;CACH;AArCD,gCAqCC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
5
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
6
|
+
class SubDirs {
|
|
7
|
+
static getSubDirs(dirPath) {
|
|
8
|
+
const contents = fs_1.default.readdirSync(dirPath);
|
|
9
|
+
const subDirectories = contents.filter((item) => {
|
|
10
|
+
const itemPath = path_1.default.join(dirPath, item);
|
|
11
|
+
return fs_1.default.statSync(itemPath).isDirectory();
|
|
12
|
+
});
|
|
13
|
+
return subDirectories;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.default = SubDirs;
|
|
17
|
+
//# sourceMappingURL=subDirs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subDirs.js","sourceRoot":"","sources":["../../src/utils/subDirs.ts"],"names":[],"mappings":";;;AAAA,oDAAsB;AACtB,wDAAwB;AAExB,MAAqB,OAAO;IACzB,MAAM,CAAC,UAAU,CAAE,OAAe;QAE/B,MAAM,QAAQ,GAAG,YAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAGzC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YAC7C,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC1C,OAAO,YAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,OAAO,cAAc,CAAC;IACzB,CAAC;CACH;AAbD,0BAaC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@companieshouse/ch-node-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "companieshouse",
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"types": "./lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"templates"
|
|
10
|
+
],
|
|
11
|
+
"description": "set of node utils which can be used across repos/projects",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/companieshouse/ch-node-utils"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prebuild": "rm -rf ./lib",
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"_test": "mocha --recursive test/**/*.spec.ts",
|
|
20
|
+
"test": "echo 'would execute: mocha --recursive test/**/*.spec.ts'",
|
|
21
|
+
"_lint": "eslint 'src/*.ts' 'src/**/*.ts' 'test/**/*.ts'",
|
|
22
|
+
"lint": "echo would execute: eslint 'src/*.ts' 'src/**/*.ts' 'test/**/*.ts'",
|
|
23
|
+
"_lint:fix": "eslint 'src/*.ts' 'src/**/*.ts' 'test/**/*.ts' --fix",
|
|
24
|
+
"lint:fix": "echo would execute: eslint 'src/*.ts' 'src/**/*.ts' 'test/**/*.ts' --fix",
|
|
25
|
+
"_coverage": "nyc npm test",
|
|
26
|
+
"coverage": "echo 'would execute: nyc npm test'",
|
|
27
|
+
"_test:coverage": "nyc --check-coverage --reporter=text-summary npm run test",
|
|
28
|
+
"test:coverage": "echo 'would execute: nyc --check-coverage --reporter=text-summary npm run test'",
|
|
29
|
+
"_coverage:report": "nyc --reporter=lcov --reporter=text npm run test",
|
|
30
|
+
"coverage:report": "echo 'would execute: nyc --reporter=lcov --reporter=text npm run test'",
|
|
31
|
+
"_analyse-code": "sonar-scanner",
|
|
32
|
+
"analyse-code": "echo 'would execute: sonar-scanner'",
|
|
33
|
+
"prepublishOnly": "[ ! -d node_modules ] && npm install; tsc",
|
|
34
|
+
"postpublish": "rm -rf lib"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@companieshouse/node-session-handler": "^4.1.9",
|
|
39
|
+
"express": "^4.18.2",
|
|
40
|
+
"i18next": "^23.4.6",
|
|
41
|
+
"i18next-fs-backend": "^2.1.5",
|
|
42
|
+
"iso-639-1": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/express": "^4.17.17",
|
|
46
|
+
"@types/i18next": "^13.0.0",
|
|
47
|
+
"@types/node": "^20.5.7",
|
|
48
|
+
"typescript": "^5.2.2"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/companieshouse/ch-node-utils/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/companieshouse/ch-node-utils#readme"
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{#
|
|
2
|
+
macro to add the lang=... query params in a url passed in as arg
|
|
3
|
+
Note: the url is amended only if the 'langInSession' var is not defined/pushed to the template.
|
|
4
|
+
If the var is defined, that means that the lang can be retrieved, in the backend,
|
|
5
|
+
from the session, so no need to add that in the url.
|
|
6
|
+
If a url is "extra-service" (it doesn't point back to the service which loaded this template)
|
|
7
|
+
then there is no session to retrieve the lang=... value from, in this case, the lang=...
|
|
8
|
+
query parm could be required even though the 'langInSession' is defined.
|
|
9
|
+
Another case is when the value stored in a session is old (i.e. the user clicked
|
|
10
|
+
a change of language), even in this scenario we want to send the lang=... value
|
|
11
|
+
via a url query param instead of retrieveing the old one from the session.
|
|
12
|
+
To force a url change regardless of anything, an optional 'force' boolean-arg is added.
|
|
13
|
+
It receives:
|
|
14
|
+
- url: the url to amend
|
|
15
|
+
- lang: the value for lang=.... (i.e. 'en') }
|
|
16
|
+
- force: to force the url amend. regardless of 'langInSession'
|
|
17
|
+
#}
|
|
18
|
+
{% macro addLangToUrl(url, lang='en', force=false) %}
|
|
19
|
+
{% if not langInSession or force %} {# check if the url amend is required #}
|
|
20
|
+
{% if url.includes('?') %} {# check if there are query params already #}
|
|
21
|
+
{% set regExp = r/([\?&]lang=)[^&]*/g %}
|
|
22
|
+
{% if regExp.test(url) %}
|
|
23
|
+
{% set url = url.replace(regExp, '$1' + lang) %} {# already present. Amend #}
|
|
24
|
+
{% else %}
|
|
25
|
+
{% set url = url + '&lang=' + lang %} {# not already present. Add it #}
|
|
26
|
+
{% endif %}
|
|
27
|
+
{% else %}
|
|
28
|
+
{% set url = url + '?lang=' + lang %} {# there are no args at all. Add it #}
|
|
29
|
+
{% endif %}
|
|
30
|
+
{% set url = url.replace(r/\?&/, '?') %} {# in case of original url just 1 single '?' #}
|
|
31
|
+
{% endif %}
|
|
32
|
+
{{ url }}
|
|
33
|
+
{% endmacro %}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{#
|
|
2
|
+
template to be reused in all the services to display the locales menu/banner.
|
|
3
|
+
It receives:
|
|
4
|
+
- languageEnabled: to keep all hidden if feature-flag is off
|
|
5
|
+
- languages: array of pairs {.Name (i.e. 'English') .IsoCode (i.e. 'en') }
|
|
6
|
+
- currentUrl: self-url. It's used to resend to the backend the same request wih the proper
|
|
7
|
+
query parameter ("lang=..") set
|
|
8
|
+
(i.e. https://.....<self>....../?lang=cy)
|
|
9
|
+
#}
|
|
10
|
+
|
|
11
|
+
<div class="govuk-width-container ">
|
|
12
|
+
<div class="govuk-grid-column-full no-padd">
|
|
13
|
+
<nav class="govuk-language-select" aria-label="Language switcher">
|
|
14
|
+
<ul class="govuk-language-select__list">
|
|
15
|
+
{% if languageEnabled %}
|
|
16
|
+
{% for l in languages %}
|
|
17
|
+
<li class="govuk-language-select__list-item">
|
|
18
|
+
<a href="{{lang2url.addLangToUrl(currentUrl, l.IsoCode, true)}}" lang={{l.IsoCode}} rel="alternate" class="govuk-link" >
|
|
19
|
+
<span aria-hidden="true">{{l.Name}}</span>
|
|
20
|
+
</a>
|
|
21
|
+
</li>
|
|
22
|
+
{% endfor %}
|
|
23
|
+
{% endif %}
|
|
24
|
+
</ul>
|
|
25
|
+
</nav>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|