Skip to content

Commit 16102ed

Browse files
committed
first implementation
1 parent e8334e0 commit 16102ed

File tree

7 files changed

+357
-1
lines changed

7 files changed

+357
-1
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ typings/
5757
# dotenv environment variables file
5858
.env
5959

60+
*.js
61+
*.js.map
62+
*.d.ts
63+
!config.d.ts

.npmignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*.ts
2+
!*.d.ts
3+
*.js.map
4+
5+
.gitignore
6+
.npmignore
7+
8+
node_modules
9+
10+
tsconfig.json
11+
12+
*.tgz

README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,49 @@
1-
# ya-i18next-webpack-plugin
1+
[![npm version](https://badge.fury.io/js/ya-i18next-webpack-plugin.svg)](https://badge.fury.io/js/ya-i18next-webpack-plugin)
2+
[![dependencies Status](https://david-dm.org/perlmint/ya-i18next-webpack-plugin/status.svg)](https://david-dm.org/perlmint/ya-i18next-webpack-plugin)
3+
[![devDependencies Status](https://david-dm.org/perlmint/ya-i18next-webpack-plugin/dev-status.svg)](https://david-dm.org/perlmint/ya-i18next-webpack-plugin?type=dev)
4+
# ya-i18next-webpack-plugin
5+
6+
Yet another i18next webpack plugin
7+
8+
This plugin collects keys from webpack parsing phase, saves missing translations into specified path, copies translation files.
9+
10+
## usage
11+
12+
### webpack.config.js
13+
```js
14+
const i18nextPlugin = require("ya-i18next-webpack-plugin").default;
15+
16+
module.exports = {
17+
plugins: [
18+
new i18nextPlugin({
19+
defaultLanguage: "en",
20+
languages: ["en", "ko"],
21+
functionName: "_t",
22+
resourcePath: "./locales/{{lng}}/{{ns}}.json",
23+
pathToSaveMissing: "./locales/{{lng}}/{{ns}} -missing.json"
24+
})
25+
]
26+
};
27+
```
28+
29+
### index.js
30+
```js
31+
import * as i18next from 'i18next';
32+
import * as Backend from 'i18next-xhr-backend';
33+
import * as i18nConf from "ya-i18next-webpack-plugin/config";
34+
35+
if (window._t === undefined) {
36+
i18next
37+
.use(Backend)
38+
.init({
39+
fallbackLng: i18nConf.DEFAULT_NAMESPACE,
40+
whitelist: i18nConf.LANGUAGES,
41+
backend: {
42+
loadPath: `${__webpack_public_path__}${i18nConf.RESOURCE_PATH}`
43+
}
44+
});
45+
window._t = i18next.t;
46+
}
47+
48+
console.log(_t("hello_world"));
49+
```

config.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export declare const RESOURCE_PATH: string;
2+
export declare const LANGUAGES: string[];
3+
export declare const DEFAULT_NAMESPACE: string;
4+

index.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import wp = require("webpack");
2+
import fs = require("fs");
3+
import path = require("path");
4+
import util = require('util');
5+
import _ = require("lodash");
6+
import i18next = require('i18next');
7+
import Backend = require('i18next-node-fs-backend');
8+
const VirtualModulePlugin = require('virtual-module-webpack-plugin');
9+
10+
const readFile = util.promisify(fs.readFile);
11+
12+
function extractArgs(arg: any, warning?: (msg: string) => void) {
13+
switch (arg.type) {
14+
case 'Literal':
15+
return arg.value;
16+
case 'Identifier':
17+
return arg.name;
18+
case 'ObjectExpression':
19+
const res: {[key: string]: string} = {};
20+
for (const i in arg.properties) {
21+
res[extractArgs(arg.properties[i].key)] = extractArgs(arg.properties[i].value);
22+
}
23+
return res;
24+
default:
25+
if (warning) {
26+
warning(`unable to parse arg ${arg}`);
27+
}
28+
return null;
29+
}
30+
}
31+
32+
export interface Option {
33+
defaultLanguage: string;
34+
/**
35+
* languages to emit
36+
*/
37+
languages: string[];
38+
defaultNamespace?: string;
39+
namespaces?: string[];
40+
/**
41+
* Scanning function name
42+
*
43+
* @default "__"
44+
*/
45+
functionName?: string;
46+
resourcePath: string;
47+
/**
48+
* save missing translations to...
49+
*/
50+
pathToSaveMissing: string;
51+
/**
52+
* change emit path
53+
* if this value is not set, emit to resourcePath
54+
*/
55+
outPath?: string;
56+
}
57+
58+
export interface InternalOption extends Option {
59+
defaultNamespace: string;
60+
namespaces: string[];
61+
outPath: string;
62+
}
63+
64+
function getPath(template: string, language?: string, namespace?: string) {
65+
if (language !== undefined) {
66+
template = template.replace("{{lng}}", language);
67+
}
68+
if (namespace !== undefined) {
69+
template = template.replace("{{ns}}", namespace);
70+
}
71+
72+
return template;
73+
}
74+
75+
export default class I18nextPlugin {
76+
protected compilation: wp.Compilation;
77+
protected option: InternalOption;
78+
protected context: string;
79+
protected missingKeys: {[language: string]: {[namespace: string]: string[]}};
80+
81+
public constructor(option: Option) {
82+
this.option = _.defaults(option, {
83+
functionName: "__",
84+
defaultNamespace: "translation",
85+
namespaces: [option.defaultNamespace || "translation"],
86+
outPath: option.resourcePath
87+
});
88+
89+
i18next.use(Backend);
90+
}
91+
92+
public apply(compiler: wp.Compiler) {
93+
// provide config via virtual module plugin
94+
compiler.apply(new VirtualModulePlugin({
95+
moduleName: path.join(__dirname, "config.js"),
96+
contents: `exports = module.exports = {
97+
__esModule: true,
98+
RESOURCE_PATH: "${this.option.outPath}",
99+
LANGUAGES: ${JSON.stringify(this.option.languages)},
100+
DEFAULT_NAMESPACE: "${this.option.defaultNamespace}"
101+
};`
102+
}));
103+
104+
i18next.init({
105+
preload: this.option.languages,
106+
ns: this.option.namespaces,
107+
fallbackLng: false,
108+
defaultNS: this.option.defaultNamespace,
109+
saveMissing: true,
110+
missingKeyHandler: this.onKeyMissing.bind(this),
111+
backend: {
112+
loadPath: this.option.resourcePath
113+
}
114+
});
115+
this.context = compiler.options.context || "";
116+
117+
compiler.plugin("compilation", (compilation, data) => {
118+
// reset for new compliation
119+
this.missingKeys = {};
120+
121+
i18next.reloadResources(this.option.languages);
122+
this.compilation = compilation;
123+
data.normalModuleFactory.plugin(
124+
"parser",
125+
(parser: any) => {
126+
parser.plugin(`call ${this.option.functionName}`, this.onTranslateFunctionCall.bind(this));
127+
}
128+
);
129+
});
130+
compiler.plugin("emit", this.onEmit.bind(this));
131+
compiler.plugin("after-emit", this.onAfterEmit.bind(this));
132+
}
133+
134+
protected onEmit(compilation: wp.Compilation, callback: (err?: Error) => void) {
135+
// emit translation files
136+
const promises: Promise<any>[] = [];
137+
138+
for (const lng of this.option.languages) {
139+
const resourceTemplate = path.join(this.context, getPath(this.option.resourcePath, lng));
140+
try {
141+
const resourceDir = path.dirname(resourceTemplate);
142+
fs.statSync(resourceDir);
143+
// compilation.contextDependencies.push(resourceDir);
144+
} catch (e) {
145+
146+
}
147+
148+
for (const ns of this.option.namespaces) {
149+
const resourcePath = getPath(resourceTemplate, undefined, ns);
150+
const outPath = getPath(this.option.outPath, lng, ns);
151+
152+
promises.push(readFile(resourcePath).then(v => {
153+
compilation.assets[outPath] = {
154+
size() { return v.length; },
155+
source() { return v; }
156+
};
157+
158+
compilation.fileDependencies.push(path.resolve(resourcePath));
159+
}).catch(() => {
160+
compilation.warnings.push(`Can't emit ${outPath}. It looks like ${resourcePath} is not exists.`);
161+
}));
162+
}
163+
}
164+
165+
Promise.all(promises).then(() => callback()).catch(callback);
166+
}
167+
168+
protected onAfterEmit(compilation: wp.Compilation, callback: (err?: Error) => void) {
169+
// write missing
170+
Promise.all(_.map(this.missingKeys, async (namespaces, lng) =>
171+
_.map(namespaces, async (keys, ns) => new Promise<void>(resolve => {
172+
const missingPath = path.join(this.context, getPath(this.option.pathToSaveMissing, lng, ns));
173+
const stream = fs.createWriteStream(missingPath, {
174+
defaultEncoding: "utf-8"
175+
});
176+
keys = _.sortedUniq(_.sortBy(keys));
177+
console.log(keys);
178+
stream.write("{\n");
179+
stream.write(_.map(keys, key => `\t"${key}": "${key}"`).join(",\n"));
180+
stream.write("\n}");
181+
182+
stream.on("close", () => resolve());
183+
184+
compilation.warnings.push(`missing translation ${keys.length} keys in ${lng}/${ns}`);
185+
}))
186+
)).then(() => callback()).catch(callback);
187+
}
188+
189+
protected onTranslateFunctionCall(expr: any) {
190+
const args = expr.arguments.map((arg: any) => extractArgs(arg, this.warningOnCompilation.bind(this)));
191+
192+
for (const lng of this.option.languages) {
193+
const keyOrKeys: string | string[] = args[0];
194+
const option: i18next.TranslationOptionsBase = Object.assign(_.defaults(args[1], {}), {
195+
lng,
196+
defaultValue: null
197+
});
198+
i18next.t(keyOrKeys, option);
199+
}
200+
}
201+
202+
protected onKeyMissing(lng: string, ns: string, key: string, __: string) {
203+
const p = [lng, ns];
204+
let arr: string[] = _.get(this.missingKeys, p);
205+
if (arr === undefined) {
206+
_.set(this.missingKeys, p, []);
207+
arr = _.get(this.missingKeys, p);
208+
}
209+
arr.push(key);
210+
}
211+
212+
protected warningOnCompilation(msg: string) {
213+
if (this.compilation) {
214+
this.compilation.warnings.push(msg);
215+
}
216+
}
217+
}

package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "ya-i18next-webpack-plugin",
3+
"version": "0.1.0",
4+
"description": "",
5+
"main": "index.js",
6+
"typings": "index.d.ts",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/Perlmint/ya-i18next-webpack-plugin.git"
13+
},
14+
"keywords": [
15+
"i18next",
16+
"webpack",
17+
"plugin",
18+
"i18n"
19+
],
20+
"author": "Gyusun Yeom <omniavinco@gmail.com>",
21+
"license": "MIT",
22+
"bugs": {
23+
"url": "https://github.com/Perlmint/ya-i18next-webpack-plugin/issues"
24+
},
25+
"homepage": "https://github.com/Perlmint/ya-i18next-webpack-plugin#readme",
26+
"devDependencies": {
27+
"@types/i18next": "^8.4.2",
28+
"@types/i18next-node-fs-backend": "0.0.29",
29+
"@types/lodash": "^4.14.74",
30+
"@types/node": "^8.0.26",
31+
"@types/webpack": "^3.0.10",
32+
"typescript": "^2.4.2"
33+
},
34+
"dependencies": {
35+
"i18next": "^9.0.0",
36+
"i18next-node-fs-backend": "^1.0.0",
37+
"lodash": "^4.17.4",
38+
"virtual-module-webpack-plugin": "^0.3.0"
39+
}
40+
}

tsconfig.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"compilerOptions": {
3+
/* Basic Options */
4+
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
5+
"module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6+
"lib": [
7+
"es5",
8+
"es2015"
9+
],
10+
"allowJs": false, /* Allow javascript files to be compiled. */
11+
"declaration": true, /* Generates corresponding '.d.ts' file. */
12+
"sourceMap": true, /* Generates corresponding '.map' file. */
13+
"removeComments": false, /* Do not emit comments to output. */
14+
15+
/* Strict Type-Checking Options */
16+
"strict": true, /* Enable all strict type-checking options. */
17+
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
18+
"strictNullChecks": true, /* Enable strict null checks. */
19+
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
20+
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
21+
22+
/* Additional Checks */
23+
"noUnusedLocals": true, /* Report errors on unused locals. */
24+
"noUnusedParameters": true, /* Report errors on unused parameters. */
25+
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
26+
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
27+
28+
/* Module Resolution Options */
29+
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
30+
}
31+
}

0 commit comments

Comments
 (0)