@remvst/localization 1.0.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/.github/workflows/check.yaml +25 -0
- package/README.md +67 -0
- package/lib/commands/combine-json.d.ts +7 -0
- package/lib/commands/combine-json.js +30 -0
- package/lib/commands/google-translate.d.ts +6 -0
- package/lib/commands/google-translate.js +20 -0
- package/lib/commands/parse-csv.d.ts +7 -0
- package/lib/commands/parse-csv.js +31 -0
- package/lib/commands/to-typescript.d.ts +4 -0
- package/lib/commands/to-typescript.js +11 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +123 -0
- package/lib/model/translation-set.d.ts +21 -0
- package/lib/model/translation-set.js +92 -0
- package/package.json +30 -0
- package/src/commands/combine-json.ts +39 -0
- package/src/commands/google-translate.ts +32 -0
- package/src/commands/parse-csv.ts +41 -0
- package/src/commands/to-typescript.ts +12 -0
- package/src/index.ts +125 -0
- package/src/model/translation-set.ts +119 -0
- package/test/localization.json +9 -0
- package/test/polyglot.csv +645 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Check
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
jobs:
|
|
8
|
+
check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
concurrency:
|
|
11
|
+
group: check-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/setup-node@v3
|
|
15
|
+
with:
|
|
16
|
+
node-version: 18
|
|
17
|
+
- name: Check out repository code
|
|
18
|
+
uses: actions/checkout@v3
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: npm install
|
|
21
|
+
- name: Create testOut
|
|
22
|
+
run: mkdir testOut
|
|
23
|
+
- name: Run tests
|
|
24
|
+
shell: bash
|
|
25
|
+
run: npm test
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @remvst/localization
|
|
2
|
+
|
|
3
|
+
Localization utils for TypeScript.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install --save-dev @remvst/localization
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Create a JSON file with your localized strings (`my-localization.json`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"ok": { "en": "Okay" },
|
|
18
|
+
"confirm": { "en": "Confirm" },
|
|
19
|
+
"cancel": { "en": "Cancel" },
|
|
20
|
+
"back": { "en": "Back" },
|
|
21
|
+
"play": { "en": "Play" },
|
|
22
|
+
"time": { "en": "Time" },
|
|
23
|
+
"backToMainMenu": { "en": "Back To Main Menu", "fr": "Retour au menu principal" }
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Parse the Polyglot CSV file into a JSON file:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx @remvst/localize parse-csv \
|
|
31
|
+
--in=polyglot.csv \
|
|
32
|
+
--out=polyglot.json \
|
|
33
|
+
--languages-line-index=1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Backfill missing translations using a fallback JSON:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npx @remvst/localize combine-json \
|
|
40
|
+
--main=localization.json \
|
|
41
|
+
--fallback=polyglot.json \
|
|
42
|
+
--out=localization-combined.json \
|
|
43
|
+
--fallback-locale=en \
|
|
44
|
+
--locale=en \
|
|
45
|
+
--locale=fr \
|
|
46
|
+
--locale=es
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Backfill missing translations using Google translate:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npx @remvst/localize google-translate \
|
|
53
|
+
--in=localization-combined.json \
|
|
54
|
+
--out=localization-full.json \
|
|
55
|
+
--fallback-locale=en \
|
|
56
|
+
--locale=en \
|
|
57
|
+
--locale=fr \
|
|
58
|
+
--locale=es
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Export to Typescript:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx @remvst/localize to-typescript \
|
|
65
|
+
--in=localization-full.json \
|
|
66
|
+
--out=localization-full.ts
|
|
67
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.combineJsonCommand = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const translation_set_1 = require("../model/translation-set");
|
|
6
|
+
async function combineJsonCommand(options) {
|
|
7
|
+
const fallbackTranslations = [];
|
|
8
|
+
for (const fallbackPath of options.fallback || []) {
|
|
9
|
+
const sourceJson = JSON.parse(await fs_1.promises.readFile(fallbackPath, 'utf-8'));
|
|
10
|
+
const translationSet = translation_set_1.TranslationSet.fromJSON(sourceJson);
|
|
11
|
+
fallbackTranslations.push(translationSet);
|
|
12
|
+
}
|
|
13
|
+
const mainJson = JSON.parse(await fs_1.promises.readFile(options.main, 'utf-8'));
|
|
14
|
+
const outTranslations = translation_set_1.TranslationSet.fromJSON(mainJson);
|
|
15
|
+
await outTranslations.applyFallbacks(options.locale || ['en'], async (key, locale, item) => {
|
|
16
|
+
const translationInFallbackLocale = item.get(options.fallbackLocale);
|
|
17
|
+
if (!translationInFallbackLocale)
|
|
18
|
+
return null;
|
|
19
|
+
for (const fallbackSet of fallbackTranslations) {
|
|
20
|
+
if (!translationInFallbackLocale)
|
|
21
|
+
continue;
|
|
22
|
+
const fromFallbackLocale = fallbackSet.fromLocalization(options.fallbackLocale, translationInFallbackLocale)?.get(locale);
|
|
23
|
+
if (fromFallbackLocale)
|
|
24
|
+
return fromFallbackLocale;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
});
|
|
28
|
+
await fs_1.promises.writeFile(options.out, JSON.stringify(outTranslations.toJSON(), null, 4));
|
|
29
|
+
}
|
|
30
|
+
exports.combineJsonCommand = combineJsonCommand;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.googleTranslateCommand = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const translation_set_1 = require("../model/translation-set");
|
|
6
|
+
const google_translate_api_1 = require("@vitalets/google-translate-api");
|
|
7
|
+
async function googleTranslateCommand(options) {
|
|
8
|
+
const mainJson = JSON.parse(await fs_1.promises.readFile(options.in, 'utf-8'));
|
|
9
|
+
const outTranslations = translation_set_1.TranslationSet.fromJSON(mainJson);
|
|
10
|
+
await outTranslations.applyFallbacks(options.locale || ['en'], async (_, locale, item) => {
|
|
11
|
+
const translationInFallbackLocale = item.get(options.fallbackLocale);
|
|
12
|
+
if (!translationInFallbackLocale)
|
|
13
|
+
return null;
|
|
14
|
+
console.log(`Translating ${JSON.stringify(translationInFallbackLocale)} to ${locale}`);
|
|
15
|
+
const translationResult = await (0, google_translate_api_1.translate)(translationInFallbackLocale, { from: options.fallbackLocale, to: locale });
|
|
16
|
+
return translationResult?.text;
|
|
17
|
+
});
|
|
18
|
+
await fs_1.promises.writeFile(options.out, JSON.stringify(outTranslations.toJSON(), null, 4));
|
|
19
|
+
}
|
|
20
|
+
exports.googleTranslateCommand = googleTranslateCommand;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TranslationSet } from "../model/translation-set";
|
|
2
|
+
export declare function parseCsv(path: string, languagesLine: number): Promise<TranslationSet>;
|
|
3
|
+
export declare function parseCsvCommand(options: {
|
|
4
|
+
in: string;
|
|
5
|
+
out: string;
|
|
6
|
+
languagesLineIndex: number;
|
|
7
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCsvCommand = exports.parseCsv = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const translation_set_1 = require("../model/translation-set");
|
|
6
|
+
async function parseCsv(path, languagesLine) {
|
|
7
|
+
const csvContent = await fs_1.promises.readFile(path, 'utf-8');
|
|
8
|
+
const csvLines = csvContent.split('\n');
|
|
9
|
+
const res = new translation_set_1.TranslationSet();
|
|
10
|
+
const ietfLine = csvLines[languagesLine];
|
|
11
|
+
const languagesIndices = new Map();
|
|
12
|
+
const ietfColumns = ietfLine.split(',');
|
|
13
|
+
for (let i = 2; i < ietfColumns.length; i++) {
|
|
14
|
+
const locale = ietfColumns[i].slice(0, 2); // only grab the first two chars to identify the language
|
|
15
|
+
languagesIndices.set(locale, i);
|
|
16
|
+
}
|
|
17
|
+
for (const line of csvLines) {
|
|
18
|
+
const columns = line.split(',');
|
|
19
|
+
const key = columns[0];
|
|
20
|
+
for (const [locale, columnIndex] of languagesIndices.entries()) {
|
|
21
|
+
res.add(key, locale, columns[columnIndex]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
exports.parseCsv = parseCsv;
|
|
27
|
+
async function parseCsvCommand(options) {
|
|
28
|
+
const polyglotTranslations = await parseCsv(options.in, options.languagesLineIndex);
|
|
29
|
+
await fs_1.promises.writeFile(options.out, JSON.stringify(polyglotTranslations, null, 4));
|
|
30
|
+
}
|
|
31
|
+
exports.parseCsvCommand = parseCsvCommand;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toTypescriptCommand = void 0;
|
|
4
|
+
const translation_set_1 = require("../model/translation-set");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
async function toTypescriptCommand(options) {
|
|
7
|
+
const sourceJson = JSON.parse(await fs_1.promises.readFile(options.in, 'utf-8'));
|
|
8
|
+
const outTranslations = translation_set_1.TranslationSet.fromJSON(sourceJson);
|
|
9
|
+
await fs_1.promises.writeFile(options.out, outTranslations.toTypeScript());
|
|
10
|
+
}
|
|
11
|
+
exports.toTypescriptCommand = toTypescriptCommand;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const helpers_1 = require("yargs/helpers");
|
|
8
|
+
const yargs_1 = __importDefault(require("yargs"));
|
|
9
|
+
const combine_json_1 = require("./commands/combine-json");
|
|
10
|
+
const google_translate_1 = require("./commands/google-translate");
|
|
11
|
+
const parse_csv_1 = require("./commands/parse-csv");
|
|
12
|
+
const to_typescript_1 = require("./commands/to-typescript");
|
|
13
|
+
async function main() {
|
|
14
|
+
await (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
15
|
+
.command('parse-csv', 'convert a CSV into a JSON', async (yargs) => {
|
|
16
|
+
const argv = await yargs
|
|
17
|
+
.option('in', {
|
|
18
|
+
type: 'string',
|
|
19
|
+
alias: 'i',
|
|
20
|
+
describe: 'Path to CSV to read from',
|
|
21
|
+
})
|
|
22
|
+
.option('out', {
|
|
23
|
+
type: 'string',
|
|
24
|
+
alias: 'o',
|
|
25
|
+
describe: 'Output file',
|
|
26
|
+
})
|
|
27
|
+
.option('languagesLineIndex', {
|
|
28
|
+
type: 'number',
|
|
29
|
+
alias: 'l',
|
|
30
|
+
describe: 'Index of the row containing the languages',
|
|
31
|
+
})
|
|
32
|
+
.demandOption('in')
|
|
33
|
+
.demandOption('out')
|
|
34
|
+
.demandOption('languagesLineIndex')
|
|
35
|
+
.argv;
|
|
36
|
+
await (0, parse_csv_1.parseCsvCommand)(argv);
|
|
37
|
+
})
|
|
38
|
+
.command('combine-json', 'combine multiple JSON files', async (yargs) => {
|
|
39
|
+
const argv = await yargs
|
|
40
|
+
.option('main', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
alias: 'i',
|
|
43
|
+
describe: 'JSON file to read from',
|
|
44
|
+
})
|
|
45
|
+
.option('out', {
|
|
46
|
+
type: 'string',
|
|
47
|
+
alias: 'o',
|
|
48
|
+
describe: 'Output JSON file',
|
|
49
|
+
})
|
|
50
|
+
.option('fallbackLocale', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
describe: 'Default locale to backfill translations',
|
|
53
|
+
default: 'en',
|
|
54
|
+
})
|
|
55
|
+
.option('fallback', {
|
|
56
|
+
type: 'string',
|
|
57
|
+
alias: 'f',
|
|
58
|
+
describe: 'Fallback JSON file to read from',
|
|
59
|
+
})
|
|
60
|
+
.option('locale', {
|
|
61
|
+
type: 'string',
|
|
62
|
+
alias: 'l',
|
|
63
|
+
describe: 'Locale to include',
|
|
64
|
+
})
|
|
65
|
+
.array('fallback')
|
|
66
|
+
.array('locale')
|
|
67
|
+
.demandOption('main')
|
|
68
|
+
.demandOption('out')
|
|
69
|
+
.demandOption('locale')
|
|
70
|
+
.demandOption('fallback')
|
|
71
|
+
.argv;
|
|
72
|
+
await (0, combine_json_1.combineJsonCommand)(argv);
|
|
73
|
+
})
|
|
74
|
+
.command('to-typescript', 'converts a JSON file into a TypeScript file', async (yargs) => {
|
|
75
|
+
const argv = await yargs
|
|
76
|
+
.option('in', {
|
|
77
|
+
type: 'string',
|
|
78
|
+
alias: 'i',
|
|
79
|
+
describe: 'JSON file to read from',
|
|
80
|
+
})
|
|
81
|
+
.options('out', {
|
|
82
|
+
type: 'string',
|
|
83
|
+
alias: 'o',
|
|
84
|
+
describe: 'Output Typescript file',
|
|
85
|
+
})
|
|
86
|
+
.demandOption('in')
|
|
87
|
+
.demandOption('out')
|
|
88
|
+
.argv;
|
|
89
|
+
await (0, to_typescript_1.toTypescriptCommand)(argv);
|
|
90
|
+
})
|
|
91
|
+
.command('google-translate', 'adds missing translations to a JSON file using Google translate', async (yargs) => {
|
|
92
|
+
const argv = await yargs
|
|
93
|
+
.option('in', {
|
|
94
|
+
type: 'string',
|
|
95
|
+
alias: 'i',
|
|
96
|
+
describe: 'JSON file to read from',
|
|
97
|
+
})
|
|
98
|
+
.option('out', {
|
|
99
|
+
type: 'string',
|
|
100
|
+
alias: 'o',
|
|
101
|
+
describe: 'Output JSON file',
|
|
102
|
+
})
|
|
103
|
+
.option('fallbackLocale', {
|
|
104
|
+
type: 'string',
|
|
105
|
+
describe: 'Default locale to backfill translations',
|
|
106
|
+
default: 'en',
|
|
107
|
+
})
|
|
108
|
+
.option('locale', {
|
|
109
|
+
type: 'string',
|
|
110
|
+
alias: 'l',
|
|
111
|
+
describe: 'Locale to include',
|
|
112
|
+
})
|
|
113
|
+
.array('fallback')
|
|
114
|
+
.array('locale')
|
|
115
|
+
.demandOption('in')
|
|
116
|
+
.demandOption('out')
|
|
117
|
+
.demandOption('locale')
|
|
118
|
+
.argv;
|
|
119
|
+
await (0, google_translate_1.googleTranslateCommand)(argv);
|
|
120
|
+
})
|
|
121
|
+
.argv;
|
|
122
|
+
}
|
|
123
|
+
main();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Key = string;
|
|
2
|
+
export type Locale = string;
|
|
3
|
+
export type LocalizedItem = Map<Locale, string>;
|
|
4
|
+
export declare class TranslationSet {
|
|
5
|
+
private readonly locales;
|
|
6
|
+
private readonly translations;
|
|
7
|
+
add(key: Key, locale: Locale, localization: string): void;
|
|
8
|
+
fromLocalization(language: Locale, localization: string): LocalizedItem | null;
|
|
9
|
+
toJSON(): {
|
|
10
|
+
[key: string]: {
|
|
11
|
+
[key: string]: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
static fromJSON(json: {
|
|
15
|
+
[key: string]: {
|
|
16
|
+
[key: string]: string;
|
|
17
|
+
};
|
|
18
|
+
}): TranslationSet;
|
|
19
|
+
toTypeScript(): string;
|
|
20
|
+
applyFallbacks(locales: Locale[], fallback: (key: Key, locale: Locale, localizedItem: LocalizedItem) => Promise<string | null>): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TranslationSet = void 0;
|
|
4
|
+
class TranslationSet {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.locales = new Set();
|
|
7
|
+
this.translations = new Map();
|
|
8
|
+
}
|
|
9
|
+
add(key, locale, localization) {
|
|
10
|
+
this.locales.add(locale);
|
|
11
|
+
if (!this.translations.has(key)) {
|
|
12
|
+
this.translations.set(key, new Map());
|
|
13
|
+
}
|
|
14
|
+
this.translations.get(key).set(locale, localization);
|
|
15
|
+
}
|
|
16
|
+
fromLocalization(language, localization) {
|
|
17
|
+
const search = localization.toLowerCase();
|
|
18
|
+
for (const localizedItem of this.translations.values()) {
|
|
19
|
+
if (localizedItem.get(language)?.toLowerCase() === search) {
|
|
20
|
+
return localizedItem;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
toJSON() {
|
|
26
|
+
const res = {};
|
|
27
|
+
for (const [key, localizedItem] of this.translations.entries()) {
|
|
28
|
+
res[key] = {};
|
|
29
|
+
for (const [locale, translation] of localizedItem.entries()) {
|
|
30
|
+
res[key][locale] = translation;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
}
|
|
35
|
+
static fromJSON(json) {
|
|
36
|
+
const res = new TranslationSet();
|
|
37
|
+
for (const [key, localizedItem] of Object.entries(json)) {
|
|
38
|
+
for (const [locale, translation] of Object.entries(localizedItem)) {
|
|
39
|
+
res.add(key, locale, translation);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return res;
|
|
43
|
+
}
|
|
44
|
+
toTypeScript() {
|
|
45
|
+
let generatedFileContent = '';
|
|
46
|
+
generatedFileContent += `export type Locale = ${Array.from(this.locales).map(key => JSON.stringify(key)).join(' | ')};\n\n`;
|
|
47
|
+
generatedFileContent += `export type LocalizedKey = ${Array.from(this.translations.keys()).map(key => JSON.stringify(key)).join(' | ')};\n\n`;
|
|
48
|
+
generatedFileContent += `export type LocalizationItem = {[key in Locale]?: string};\n\n`;
|
|
49
|
+
generatedFileContent += 'export const LOCALIZATION: {[key in LocalizedKey]: LocalizationItem} = {\n';
|
|
50
|
+
for (const [key, localizedItem] of this.translations.entries()) {
|
|
51
|
+
generatedFileContent += ` ${key}: {\n`;
|
|
52
|
+
for (const [locale, translation] of localizedItem.entries()) {
|
|
53
|
+
if (!translation)
|
|
54
|
+
continue;
|
|
55
|
+
generatedFileContent += ` ${locale}: ${JSON.stringify(translation)},\n`;
|
|
56
|
+
}
|
|
57
|
+
generatedFileContent += ` },\n`;
|
|
58
|
+
}
|
|
59
|
+
generatedFileContent += '};\n\n';
|
|
60
|
+
generatedFileContent += `let LOCALE: Locale = 'en';\n\n`;
|
|
61
|
+
generatedFileContent += `export function setLocale(locale: string) {\n`;
|
|
62
|
+
generatedFileContent += ` LOCALE = locale.split('-')[0] as Locale\n`;
|
|
63
|
+
generatedFileContent += `}\n\n`;
|
|
64
|
+
generatedFileContent += `export function localize(key: LocalizedKey) {\n`;
|
|
65
|
+
generatedFileContent += ` const localizationItem: LocalizationItem = LOCALIZATION[key];\n`;
|
|
66
|
+
generatedFileContent += ` return localizationItem[LOCALE] || localizationItem.en;\n`;
|
|
67
|
+
generatedFileContent += `}\n`;
|
|
68
|
+
return generatedFileContent;
|
|
69
|
+
}
|
|
70
|
+
async applyFallbacks(locales, fallback) {
|
|
71
|
+
for (const [key, localizedItem] of this.translations.entries()) {
|
|
72
|
+
for (const locale of locales) {
|
|
73
|
+
let translation = localizedItem.get(locale) || null;
|
|
74
|
+
if (translation)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
translation = await fallback(key, locale, localizedItem);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error(err);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!translation) {
|
|
84
|
+
console.warn(`Unable to find fallback for ${key}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
this.add(key, locale, translation);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.TranslationSet = TranslationSet;
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remvst/localization",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"localization": "./lib/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rm -rf lib && tsc",
|
|
11
|
+
"prepublishOnly": "npm i && npm run build",
|
|
12
|
+
"test:polyglot": "ts-node src/index.ts index -i test/polyglot.csv -o testOut/polyglot.json -l 1",
|
|
13
|
+
"test:backfill-json": "ts-node src/index.ts combine-json --main=test/localization.json --out=testOut/localization-backfilled.json --fallback=testOut/polyglot.json -l en -l fr -l es",
|
|
14
|
+
"test:backfill-google-translate": "ts-node src/index.ts google-translate --in=testOut/localization-backfilled.json --out=testOut/localization-backfilled-google-translate.json -l en -l fr -l es",
|
|
15
|
+
"test:to-typescript": "ts-node src/index.ts to-typescript -i testOut/localization-backfilled.json -o testOut/localization.ts",
|
|
16
|
+
"test": "npm run test:polyglot && npm run test:backfill-json && npm run test:to-typescript"
|
|
17
|
+
},
|
|
18
|
+
"author": "Rémi Vansteelandt",
|
|
19
|
+
"license": "UNLICENSED",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@vitalets/google-translate-api": "^9.2.0",
|
|
22
|
+
"yargs": "^17.7.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^18.11.5",
|
|
26
|
+
"@types/yargs": "^17.0.32",
|
|
27
|
+
"ts-node": "^10.9.1",
|
|
28
|
+
"typescript": "^5.2.2"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { TranslationSet, Key, Locale, LocalizedItem } from '../model/translation-set';
|
|
3
|
+
|
|
4
|
+
export async function combineJsonCommand(options: {
|
|
5
|
+
main: string,
|
|
6
|
+
out: string,
|
|
7
|
+
locale: string[],
|
|
8
|
+
fallbackLocale: string,
|
|
9
|
+
fallback: string[],
|
|
10
|
+
}) {
|
|
11
|
+
const fallbackTranslations: TranslationSet[] = [];
|
|
12
|
+
|
|
13
|
+
for (const fallbackPath of options.fallback || []) {
|
|
14
|
+
const sourceJson = JSON.parse(await fs.readFile(fallbackPath, 'utf-8'));
|
|
15
|
+
const translationSet = TranslationSet.fromJSON(sourceJson);
|
|
16
|
+
fallbackTranslations.push(translationSet);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const mainJson = JSON.parse(await fs.readFile(options.main, 'utf-8'));
|
|
20
|
+
const outTranslations = TranslationSet.fromJSON(mainJson);
|
|
21
|
+
|
|
22
|
+
await outTranslations.applyFallbacks(
|
|
23
|
+
options.locale || ['en'],
|
|
24
|
+
async (key: Key, locale: Locale, item: LocalizedItem) => {
|
|
25
|
+
const translationInFallbackLocale = item.get(options.fallbackLocale);
|
|
26
|
+
if (!translationInFallbackLocale) return null;
|
|
27
|
+
|
|
28
|
+
for (const fallbackSet of fallbackTranslations) {
|
|
29
|
+
if (!translationInFallbackLocale) continue;
|
|
30
|
+
const fromFallbackLocale = fallbackSet.fromLocalization(options.fallbackLocale, translationInFallbackLocale)?.get(locale);
|
|
31
|
+
if (fromFallbackLocale) return fromFallbackLocale;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(options.out, JSON.stringify(outTranslations.toJSON(), null, 4));
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { Key, Locale, LocalizedItem, TranslationSet } from "../model/translation-set";
|
|
3
|
+
import { translate } from '@vitalets/google-translate-api';
|
|
4
|
+
|
|
5
|
+
export async function googleTranslateCommand(options: {
|
|
6
|
+
in: string,
|
|
7
|
+
out: string,
|
|
8
|
+
locale: string[],
|
|
9
|
+
fallbackLocale: string,
|
|
10
|
+
}) {
|
|
11
|
+
const mainJson = JSON.parse(await fs.readFile(options.in, 'utf-8'));
|
|
12
|
+
const outTranslations = TranslationSet.fromJSON(mainJson);
|
|
13
|
+
|
|
14
|
+
await outTranslations.applyFallbacks(
|
|
15
|
+
options.locale || ['en'],
|
|
16
|
+
async (_: Key, locale: Locale, item: LocalizedItem) => {
|
|
17
|
+
const translationInFallbackLocale = item.get(options.fallbackLocale);
|
|
18
|
+
if (!translationInFallbackLocale) return null;
|
|
19
|
+
|
|
20
|
+
console.log(`Translating ${JSON.stringify(translationInFallbackLocale)} to ${locale}`);
|
|
21
|
+
|
|
22
|
+
const translationResult = await translate(
|
|
23
|
+
translationInFallbackLocale,
|
|
24
|
+
{ from: options.fallbackLocale, to: locale },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return translationResult?.text;
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await fs.writeFile(options.out, JSON.stringify(outTranslations.toJSON(), null, 4));
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { TranslationSet } from "../model/translation-set";
|
|
3
|
+
|
|
4
|
+
export async function parseCsv(
|
|
5
|
+
path: string,
|
|
6
|
+
languagesLine: number,
|
|
7
|
+
): Promise<TranslationSet> {
|
|
8
|
+
const csvContent = await fs.readFile(path, 'utf-8');
|
|
9
|
+
const csvLines = csvContent.split('\n');
|
|
10
|
+
|
|
11
|
+
const res = new TranslationSet();
|
|
12
|
+
|
|
13
|
+
const ietfLine = csvLines[languagesLine];
|
|
14
|
+
const languagesIndices = new Map<string, number>();
|
|
15
|
+
const ietfColumns = ietfLine.split(',');
|
|
16
|
+
for (let i = 2 ; i < ietfColumns.length ; i++) {
|
|
17
|
+
const locale = ietfColumns[i].slice(0, 2); // only grab the first two chars to identify the language
|
|
18
|
+
languagesIndices.set(locale, i);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const line of csvLines) {
|
|
22
|
+
const columns = line.split(',');
|
|
23
|
+
|
|
24
|
+
const key = columns[0];
|
|
25
|
+
|
|
26
|
+
for (const [locale, columnIndex] of languagesIndices.entries()) {
|
|
27
|
+
res.add(key, locale, columns[columnIndex]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return res;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function parseCsvCommand(options: {
|
|
35
|
+
in: string,
|
|
36
|
+
out: string,
|
|
37
|
+
languagesLineIndex: number,
|
|
38
|
+
}) {
|
|
39
|
+
const polyglotTranslations = await parseCsv(options.in!, options.languagesLineIndex);
|
|
40
|
+
await fs.writeFile(options.out!, JSON.stringify(polyglotTranslations, null, 4));
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TranslationSet } from "../model/translation-set";
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function toTypescriptCommand(options: {
|
|
5
|
+
in: string,
|
|
6
|
+
out: string,
|
|
7
|
+
}) {
|
|
8
|
+
const sourceJson = JSON.parse(await fs.readFile(options.in, 'utf-8'));
|
|
9
|
+
const outTranslations = TranslationSet.fromJSON(sourceJson);
|
|
10
|
+
|
|
11
|
+
await fs.writeFile(options.out, outTranslations.toTypeScript());
|
|
12
|
+
}
|