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
+ }
0 commit comments