Skip to content

Commit 5a09220

Browse files
committed
Completely restructured project
1 parent 242dd49 commit 5a09220

14 files changed

+232
-24
lines changed

.github/workflows/on-push.yml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: On Push
2+
on: [push]
3+
jobs:
4+
build:
5+
name: Building
6+
runs-on: Linux
7+
steps:
8+
- name: Checking out repository
9+
uses: actions/checkout@v3
10+
- name: "Setup Node"
11+
uses: actions/setup-node@v3
12+
with:
13+
node-version: '16'
14+
cache: 'npm'
15+
- name: Installing
16+
run: npm install -save --save-dev
17+
- name: Building
18+
run: npm run build
19+
- name: Done
20+
run: exit 0
21+
22+
testing:
23+
name: Testing
24+
needs: build
25+
runs-on: Linux
26+
steps:
27+
- name: "Setup Node"
28+
uses: actions/setup-node@v3
29+
with:
30+
node-version: '16'
31+
cache: 'npm'
32+
- name: Installing
33+
run: npm install -save --save-dev
34+
- name: Running linter
35+
run: npm run lint
36+
- name: Running tests
37+
run: npm run test
38+
- name: Done
39+
run: exit 0

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
2-
lib/
2+
lib/
3+
coverage/

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
![GitHub package.json version](https://img.shields.io/github/package-json/v/LupCode/node-lup-language)
2+
![npm bundle size](https://img.shields.io/bundlephobia/min/lup-language)
3+
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/LupCode/node-lup-language/On%20Push)
4+
![NPM](https://img.shields.io/npm/l/lup-language)
5+
16
# lup-language
27
Node express middleware for detecting requested language based on:
38
1. URI prefix e.g. /**en**/home/

jestconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"^.+\\.(t|j)sx?$": "ts-jest"
44
},
55
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
6-
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
6+
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
7+
"collectCoverage":true
78
}

package-lock.json

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"homepage": "https://github.com/LupCode/node-lup-language#readme",
4040
"dependencies": {
4141
"fs": "0.0.1-security",
42-
"lup-root": "^1.3.0"
42+
"lup-root": "^1.3.2"
4343
},
4444
"devDependencies": {
4545
"@types/jest": "^27.5.0",

src/__tests__/Basic.test.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {ROOT} from 'lup-root';
2+
import * as lupLang from "../index";
3+
4+
const TRANSLATIONS_DIR = ROOT+"src/__tests__/translations";
5+
6+
test("Loading languages from translations directory", async() => {
7+
await expect(lupLang.reloadTranslations(TRANSLATIONS_DIR)).resolves.not.toThrow();
8+
});
9+
10+
11+
describe("Checking loaded translations", () => {
12+
13+
beforeAll(async() => {
14+
await lupLang.reloadTranslations(TRANSLATIONS_DIR);
15+
});
16+
17+
test("Language codes correctly determined", async() => {
18+
const languageNames = await lupLang.getLanguageNames(TRANSLATIONS_DIR);
19+
expect(languageNames).toBeInstanceOf(Object);
20+
const langs = Object.keys(languageNames);
21+
expect(langs.length).toBe(2);
22+
expect(langs).toContain("en");
23+
expect(langs).toContain("de");
24+
});
25+
26+
test("Load all translations if no translation keys are specified", async() => {
27+
const TEXT = await lupLang.getTranslations("de", "en", [], TRANSLATIONS_DIR);
28+
expect(TEXT).toBeInstanceOf(Object);
29+
expect(Object.keys(TEXT).length).toBeGreaterThanOrEqual(3);
30+
});
31+
32+
test("Only load specified translation keys", async() => {
33+
const TEXT = await lupLang.getTranslations("de", "en", ["HelloWorld"], TRANSLATIONS_DIR);
34+
expect(TEXT).toBeInstanceOf(Object);
35+
expect(Object.keys(TEXT).length).toBe(1);
36+
expect(TEXT['HelloWorld']).toBeDefined();
37+
});
38+
39+
test("Provide translation key if no translation found", async() => {
40+
const TEXT = await lupLang.getTranslations("de", "en", ["GoodNight"], TRANSLATIONS_DIR);
41+
expect(TEXT).toBeInstanceOf(Object);
42+
expect(TEXT['GoodNight']).toEqual("GoodNight");
43+
});
44+
45+
test("Translation keys in correct language loaded", async() => {
46+
const TEXT = await lupLang.getTranslations("de", "en", [], TRANSLATIONS_DIR);
47+
expect(TEXT).toBeInstanceOf(Object);
48+
expect(TEXT['HelloWorld']).toEqual("Hallo Welt");
49+
});
50+
51+
test("Global variables loaded", async() => {
52+
const TEXT = await lupLang.getTranslations("de", "en", [], TRANSLATIONS_DIR);
53+
expect(TEXT).toBeInstanceOf(Object);
54+
expect(TEXT['NAME']).toEqual("lup-language");
55+
});
56+
57+
test("Language specific translations can overwrite global variables", async() => {
58+
const TEXT = await lupLang.getTranslations("de", "en", [], TRANSLATIONS_DIR);
59+
expect(TEXT).toBeInstanceOf(Object);
60+
expect(TEXT['OverwriteMe']).toEqual("Hab ich gemacht");
61+
});
62+
63+
});
64+
65+
66+
test("Load HTML file async", async() => {
67+
const content = await lupLang.getTranslationFileContent("./html/test-de.html", TRANSLATIONS_DIR);
68+
expect(content.indexOf("Hallo Welt")).toBeGreaterThan(-1);
69+
});

src/__tests__/LanguageRouter.test.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {ROOT} from 'lup-root';
2+
import { DEFAULT_COOKIE_NAME, DEFAULT_LANGUAGE, DEFAULT_REQUEST_LANGUAGE_ATTR, DEFAULT_REQUEST_PROCESSED_PATH_ATTR, DEFAULT_REQUEST_TRANSLATIONS_ATTR, LanguageRouter } from '../index';
3+
4+
const TRANSLATIONS_DIR = ROOT+"src/__tests__/translations";
5+
6+
7+
var handle: Function;
8+
beforeAll(async() => {
9+
handle = await LanguageRouter({
10+
translationsDir: TRANSLATIONS_DIR
11+
});
12+
});
13+
14+
15+
const emulateRequestResponse = function(url: string, langCookieValue: string | null, acceptLangHeader: string | null): [req: any, res: any, exec: Function]{
16+
const req: any = new Object();
17+
req.url = url;
18+
req.headers = {};
19+
if(acceptLangHeader) req.headers["accept-language"] = acceptLangHeader;
20+
if(langCookieValue) req.headers['cookie'] = DEFAULT_COOKIE_NAME+"="+langCookieValue;
21+
22+
const res: any = new Object();
23+
res.get = (key: string) => res[key];
24+
res.set = (key: string, value: any) => res[key] = value;
25+
26+
return [req, res, function(){
27+
handle(req, res);
28+
}];
29+
}
30+
31+
32+
test("LanguageRouter valid handle", () => {
33+
expect(handle).not.toBeNull();
34+
expect(handle).toBeInstanceOf(Function);
35+
});
36+
37+
test("Detect correct language from URI", () => {
38+
const [req, _res, exec] = emulateRequestResponse("/de/hello", "en", null);
39+
expect(exec).not.toThrow();
40+
expect(req[DEFAULT_REQUEST_LANGUAGE_ATTR]).toBe("de");
41+
});
42+
43+
test("Detect correct language from cookie", () => {
44+
const [req, _res, exec] = emulateRequestResponse("/hello", "de", null);
45+
expect(exec).not.toThrow();
46+
expect(req[DEFAULT_REQUEST_LANGUAGE_ATTR]).toBe("de");
47+
});
48+
49+
test("Detect correct language from HTTP Accept-Language header field", () => {
50+
const [req, _res, exec] = emulateRequestResponse("/hello", null, "de");
51+
expect(exec).not.toThrow();
52+
expect(req[DEFAULT_REQUEST_LANGUAGE_ATTR]).toBe("de");
53+
});
54+
55+
test("Fallback to default language if no language detected", () => {
56+
const [req, _res, exec] = emulateRequestResponse("/hello", null, null);
57+
expect(exec).not.toThrow();
58+
expect(req[DEFAULT_REQUEST_LANGUAGE_ATTR]).toBe(DEFAULT_LANGUAGE);
59+
});
60+
61+
test("Translations attribute set", () => {
62+
const [req, _res, exec] = emulateRequestResponse("/de/hello", null, null);
63+
expect(exec).not.toThrow();
64+
expect(req[DEFAULT_REQUEST_TRANSLATIONS_ATTR]).toBeDefined();
65+
expect(req[DEFAULT_REQUEST_TRANSLATIONS_ATTR]['HelloWorld']).toEqual("Hallo Welt");
66+
});
67+
68+
test("Path attribute set", () => {
69+
const [req, _res, exec] = emulateRequestResponse("/de/hello?test", null, null);
70+
expect(exec).not.toThrow();
71+
expect(req[DEFAULT_REQUEST_PROCESSED_PATH_ATTR]).toBeDefined();
72+
expect(req[DEFAULT_REQUEST_PROCESSED_PATH_ATTR]).toEqual("/hello");
73+
});

src/__tests__/translations/de.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"HelloWorld": "Hallo Welt",
3+
"OverwriteMe": "Hab ich gemacht"
4+
}

src/__tests__/translations/en.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"HelloWorld": "Hello World",
3+
"OverwriteMe": "Did it"
4+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"NAME": "lup-language",
3+
"OverwriteMe": "Overwrite me"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
<h1>Hallo Welt</h1>
4+
</body>
5+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
<h1>Hello World</h1>
4+
</body>
5+
</html>

src/index.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const DICTONARY: any = {}; // { translationsDir: {lang: {key: translation} } }
6363
* @param {String} translationsDir Relative path to directory containing JSON files with translations
6464
* @returns Promise that resolves with a list of language codes that where found after translations have been reloaded from files
6565
*/
66-
export const reloadTranslations = async (translationsDir: string = DEFAULT_TRANSLATIONS_DIR): Promise<void> => {
66+
export const reloadTranslations = async (translationsDir: string = DEFAULT_TRANSLATIONS_DIR): Promise<string[]> => {
6767
if (!translationsDir) translationsDir = DEFAULT_TRANSLATIONS_DIR;
6868
LANGUAGES[translationsDir] = [];
6969
DICTONARY[translationsDir] = [];
@@ -135,7 +135,7 @@ const _getTranslations = (
135135
defaultLang: string,
136136
translationKeys: string[] | false,
137137
translationsDir: string = DEFAULT_TRANSLATIONS_DIR,
138-
) => {
138+
): {[key: string]: string} => {
139139
translationKeys = !translationKeys || translationKeys.length === 0 ? false : translationKeys;
140140
const dictornary = DICTONARY[translationsDir]
141141
? DICTONARY[translationsDir][lang] || DICTONARY[translationsDir][defaultLang] || {}
@@ -156,9 +156,9 @@ const _getTranslations = (
156156
export const getTranslations = async (
157157
lang: string,
158158
defaultLang: string,
159-
translationKeys = [],
159+
translationKeys: string[] = [],
160160
translationsDir: string = DEFAULT_TRANSLATIONS_DIR,
161-
): Promise<{}> => {
161+
): Promise<{[key: string]: string}> => {
162162
if (!translationsDir) translationsDir = DEFAULT_TRANSLATIONS_DIR;
163163
if (!DICTONARY[translationsDir]) await reloadTranslations(translationsDir);
164164
return _getTranslations(lang, defaultLang, translationKeys, translationsDir);
@@ -169,7 +169,7 @@ export const getTranslations = async (
169169
* Looksup following keys in the translations 'LANGUAGE_NAME_<lang>'
170170
* @returns {<lang>: "<native name>"}
171171
*/
172-
export const getLanguageNames = async (translationsDir: string = DEFAULT_TRANSLATIONS_DIR): Promise<{}> => {
172+
export const getLanguageNames = async (translationsDir: string = DEFAULT_TRANSLATIONS_DIR): Promise<Object> => {
173173
if (!translationsDir) translationsDir = DEFAULT_TRANSLATIONS_DIR;
174174
if (!DICTONARY[translationsDir]) await reloadTranslations(translationsDir);
175175

@@ -248,7 +248,7 @@ export const getTranslationFileContentSync = (
248248
* (if not defined 'DEFAULT_REQUEST_PROCESSED_PATH_ATTR' will be used) <br>
249249
* @returns function(req, res, next) that is designed for being set as middleware to pre-handle incoming requests
250250
*/
251-
export const LanguageRouter = (
251+
export const LanguageRouter = async(
252252
options: any = {
253253
default: DEFAULT_LANGUAGE,
254254
languages: DEFAULT_LANGUAGES,
@@ -302,13 +302,11 @@ export const LanguageRouter = (
302302
const languages = new Set(languagesArr);
303303

304304
if (loadTranslations) {
305-
reloadTranslations(translationsDir).then((ls: any) => {
306-
if (!languagesFromTranslations) return;
307-
for (const l of ls) languages.add(l);
308-
});
305+
const ls = await reloadTranslations(translationsDir);
306+
if (languagesFromTranslations) for (const l of ls) languages.add(l);
309307
}
310308

311-
return (req: any, res: any, next: any) => {
309+
return (req: any, res: any, next?: any) => {
312310
// Parse URI
313311
let lang = useUri ? req.url : false;
314312
if (lang) {
@@ -389,7 +387,7 @@ export const LanguageRouter = (
389387

390388
if (loadTranslations) req[translationsAttr] = _getTranslations(lang, defaultLang, [], translationsDir);
391389

392-
next();
390+
if(next) next();
393391
};
394392
};
395393

0 commit comments

Comments
 (0)