@elementor/extract-i18n-wordpress-expressions-webpack-plugin 0.2.2
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/CHANGELOG.md +19 -0
- package/LICENSE +674 -0
- package/README.md +4 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +130 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/__tests__/__snapshots__/index.test.ts.snap +16 -0
- package/src/__tests__/index.test.ts +82 -0
- package/src/index.ts +201 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Compilation, Compiler, Chunk, Module as WebpackModule } from 'webpack';
|
|
4
|
+
|
|
5
|
+
type Module = WebpackModule & {
|
|
6
|
+
userRequest?: string;
|
|
7
|
+
rawRequest?: string;
|
|
8
|
+
_source?: {
|
|
9
|
+
_valueAsString?: string;
|
|
10
|
+
},
|
|
11
|
+
modules?: Module[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type TranslationCallExpression = {
|
|
15
|
+
type: 'comment' | 'call-expression';
|
|
16
|
+
index: number;
|
|
17
|
+
value: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type TranslationCallExpressions = Map<string, TranslationCallExpression[]>;
|
|
21
|
+
|
|
22
|
+
const MODULE_FILTERS = Object.freeze( [ /(([^!?\s]+?)(?:\.js|\.jsx|\.ts|\.tsx))$/, /^((?!node_modules).)*$/ ] );
|
|
23
|
+
|
|
24
|
+
const COMMENTS_REGEXPS = Object.freeze( [
|
|
25
|
+
// Matches translators comment block: `/* translators: %s */`.
|
|
26
|
+
/\/\*[\t ]*translators:.*\*\//gm,
|
|
27
|
+
/// Matches translators inline comment: `// translators: %s`.
|
|
28
|
+
/(\/\/)[\t ]*translators:[^\r\n]*/gm,
|
|
29
|
+
] );
|
|
30
|
+
|
|
31
|
+
const TRANSLATIONS_REGEXPS = Object.freeze( [
|
|
32
|
+
// Matches translation functions: `__('Hello', 'elementor')`, `_n('Me', 'Us', 2, 'elementor-pro')`.
|
|
33
|
+
/\b_(?:_|n|nx|x)\(.*?,\s*(?<c>['"`])[\w-]+\k<c>\)/gm,
|
|
34
|
+
] );
|
|
35
|
+
|
|
36
|
+
export class ExtractI18nWordpressExpressionsWebpackPlugin {
|
|
37
|
+
apply( compiler: Compiler ) {
|
|
38
|
+
// Learn more about Webpack plugin system: https://webpack.js.org/api/plugins/
|
|
39
|
+
|
|
40
|
+
let translationCallExpressions: TranslationCallExpressions = new Map();
|
|
41
|
+
|
|
42
|
+
// Learn more about Webpack compilation process and hooks: https://webpack.js.org/api/compilation-hooks/
|
|
43
|
+
compiler.hooks.thisCompilation.tap( this.constructor.name, ( compilation ) => {
|
|
44
|
+
// We tap into the time that Webpack has finished processing all the other assets
|
|
45
|
+
// learn more: https://webpack.js.org/api/compilation-hooks/#processassets.
|
|
46
|
+
compilation.hooks.processAssets.tap( { name: this.constructor.name }, () => {
|
|
47
|
+
translationCallExpressions = this.getModuleExpressionsMap( compilation );
|
|
48
|
+
} );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
compiler.hooks.afterEmit.tapPromise( this.constructor.name, async ( compilation ) => {
|
|
52
|
+
// Create all the translations files based on the call expressions.
|
|
53
|
+
await this.createTranslationsFiles( compilation, translationCallExpressions );
|
|
54
|
+
} );
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getModuleExpressionsMap( compilation: Compilation ) {
|
|
58
|
+
const moduleExpressionsMap = new Map();
|
|
59
|
+
|
|
60
|
+
[ ...compilation.chunks ].forEach( ( chunk ) => {
|
|
61
|
+
const chunkJSFile = this.getFileFromChunk( chunk );
|
|
62
|
+
|
|
63
|
+
if ( ! chunkJSFile ) {
|
|
64
|
+
// There's no JS file in this chunk, no work for us. Typically a `style.css` from cache group.
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
compilation.chunkGraph.getChunkModules( chunk ).forEach( ( module ) => {
|
|
69
|
+
this.getSubModulesToCheck( module ).forEach( ( subModule ) => {
|
|
70
|
+
const mainEntryFile = this.findMainModuleOfEntry( subModule, compilation );
|
|
71
|
+
|
|
72
|
+
if ( ! moduleExpressionsMap.has( mainEntryFile ) ) {
|
|
73
|
+
moduleExpressionsMap.set( mainEntryFile, [] );
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Running over the submodules and find all the translation call expressions and their translators comment
|
|
77
|
+
// (e.g `/* translators: %s: name*/ __('Hello %s', 'elementor')`),
|
|
78
|
+
// extract them and add them to a Map, where the key is the main entry file, and the value is an array of all the
|
|
79
|
+
// translation call expressions.
|
|
80
|
+
this.extractExpressionsFromSubmodule( subModule ).forEach( ( expression ) => {
|
|
81
|
+
moduleExpressionsMap.get( mainEntryFile ).push( expression );
|
|
82
|
+
} );
|
|
83
|
+
} );
|
|
84
|
+
} );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
return moduleExpressionsMap;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async createTranslationsFiles( compilation: Compilation, translationCallExpressions: TranslationCallExpressions ) {
|
|
91
|
+
const promises = [ ...compilation.entrypoints ].map( ( [ id, entrypoint ] ) => {
|
|
92
|
+
const chunk = entrypoint.chunks.find( ( { name } ) => name === id );
|
|
93
|
+
|
|
94
|
+
if ( ! chunk ) {
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const chunkJSFile = this.getFileFromChunk( chunk );
|
|
99
|
+
|
|
100
|
+
if ( ! chunkJSFile ) {
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { entry } = compilation.options;
|
|
105
|
+
|
|
106
|
+
if ( ! entry || typeof entry !== 'object' || ! ( id in entry ) ) {
|
|
107
|
+
throw new Error( `Entry must be an object. e.g: {app: './path/to/app.js'}` );
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const mainFilePath = entry[ id ].import?.[ 0 ];
|
|
111
|
+
|
|
112
|
+
if ( ! mainFilePath ) {
|
|
113
|
+
throw new Error( 'Entry is invalid' );
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { path: outputPath } = compilation.options.output;
|
|
117
|
+
|
|
118
|
+
if ( ! outputPath ) {
|
|
119
|
+
throw new Error( 'Output path is invalid' );
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const assetFilename = this.generateTranslationFilename(
|
|
123
|
+
outputPath,
|
|
124
|
+
compilation.getPath( '[file]', { filename: chunkJSFile } )
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const assetFileContent = this.generateTranslationFileContent(
|
|
128
|
+
translationCallExpressions.get( mainFilePath ) || []
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return fs.promises.writeFile( assetFilename, assetFileContent );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
return await Promise.all( promises );
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getSubModulesToCheck( module: Module ) {
|
|
138
|
+
return [ ...( module.modules || [] ), module ]
|
|
139
|
+
.filter( ( subModule ) => this.shouldCheckModule( subModule ) );
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getFileFromChunk( chunk: Chunk ) {
|
|
143
|
+
return [ ...chunk.files ].find( ( f ) => /\.js$/i.test( f ) );
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
shouldCheckModule( module: Module ) {
|
|
147
|
+
return MODULE_FILTERS.every( ( filter ) => filter.test( module.userRequest || '' ) );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
findMainModuleOfEntry( module: Module, compilation: Compilation ) : string | null {
|
|
151
|
+
const issuer = compilation.moduleGraph.getIssuer( module );
|
|
152
|
+
|
|
153
|
+
if ( issuer ) {
|
|
154
|
+
return this.findMainModuleOfEntry( issuer, compilation );
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return module.rawRequest || null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
extractExpressionsFromSubmodule( subModule: Module ) {
|
|
161
|
+
const source = subModule?._source?._valueAsString;
|
|
162
|
+
|
|
163
|
+
if ( ! source ) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const translationCallExpressions: TranslationCallExpression[] = [];
|
|
168
|
+
|
|
169
|
+
[
|
|
170
|
+
...TRANSLATIONS_REGEXPS,
|
|
171
|
+
...COMMENTS_REGEXPS,
|
|
172
|
+
].forEach( ( regexp ) => {
|
|
173
|
+
[ ...source.matchAll( regexp ) ].forEach( ( res ) => {
|
|
174
|
+
translationCallExpressions.push( {
|
|
175
|
+
type: COMMENTS_REGEXPS.includes( regexp ) ? 'comment' : 'call-expression',
|
|
176
|
+
index: res.index || 0,
|
|
177
|
+
value: res[ 0 ],
|
|
178
|
+
} );
|
|
179
|
+
} );
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
return translationCallExpressions;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
generateTranslationFilename( basePath: string, filename: string ) {
|
|
186
|
+
return path.join(
|
|
187
|
+
basePath,
|
|
188
|
+
filename.replace( /(\.min)?\.js$/i, '.strings.js' )
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
generateTranslationFileContent( expressions: TranslationCallExpression[] ) {
|
|
193
|
+
return expressions
|
|
194
|
+
// Sort by the index it was found in the file based on the regexp (and not by the order it was added to the array).
|
|
195
|
+
.sort( ( a, b ) => a.index - b.index )
|
|
196
|
+
// Add a semicolon when needed.
|
|
197
|
+
.map( ( expr ) => `${ expr.value }${ expr.type === 'comment' ? '' : ';' }` )
|
|
198
|
+
// Join all the expressions to a single string with line-breaks between them.
|
|
199
|
+
.join( '\n' );
|
|
200
|
+
}
|
|
201
|
+
}
|