UNPKG

11.2 kBJavaScriptView Raw
1/**
2 * @file Module to replace translatable strings in HTML and JS files with
3 * translated strings. Adapted from angular-gettext-tools/extract.js.
4 *
5 * @version 0.1
6 * @since 0.1
7 * @author Sergey Popov <sergey.popov@aofl.com>
8 * @module lib/translate/impeller
9 */
10
11'use strict';
12
13var cheerio = require('cheerio');
14var espree = require('espree');
15var _ = require('lodash');
16
17var escapeRegex = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
18
19function mkAttrRegex(startDelim, endDelim) {
20 var start = startDelim.replace(escapeRegex, '\\$&');
21 var end = endDelim.replace(escapeRegex, '\\$&');
22
23 if (start === '' && end === '') {
24 start = '^';
25 } else {
26 // match optional :: (Angular 1.3's bind once syntax) without capturing
27 start += '(?:\\s*\\:\\:\\s*)?';
28 }
29
30 return new RegExp(start +
31 '\\s*(\'|"|&quot;|&#39;)(.*?)\\1\\s*\\|\\s*translate\\s*(' +
32 end + '|\\|)', 'g');
33}
34
35var noDelimRegex = mkAttrRegex('', '');
36
37function walkJs(node, fn, parentComment) {
38 fn(node, parentComment);
39 for (var key in node) {
40 if (node.hasOwnProperty(key)) {
41 var obj = node[key];
42 if (node && node.leadingComments) {
43 parentComment = node;
44 }
45 if (typeof obj === 'object') {
46 walkJs(obj, fn, parentComment);
47 }
48 }
49 }
50}
51
52function getJSExpression(node) {
53 var res = '';
54 if (node.type === 'Literal') {
55 res = node.value;
56 }
57 return res;
58}
59
60function getJSExpressionString(node) {
61 var res = '';
62 if (node.type === 'Literal') {
63 res = node.raw;
64 }
65 return res;
66}
67
68var Impeller = (function () {
69 function Impeller(options) {
70 this.options = _.extend({
71 startDelim: '{{',
72 endDelim: '}}',
73 markerName: 'gettext',
74 markerNames: [],
75 attribute: 'translate',
76 attributes: [],
77 lineNumbers: true,
78 extensions: {
79 htm: 'html',
80 html: 'html',
81 php: 'html',
82 phtml: 'html',
83 tml: 'html',
84 ejs: 'html',
85 erb: 'html',
86 js: 'js'
87 },
88 postProcess: function () {}
89 }, options);
90 this.options.markerNames.unshift(this.options.markerName);
91 this.options.attributes.unshift(this.options.attribute);
92
93 this.strings = {};
94 this.attrRegex = mkAttrRegex(this.options.startDelim,
95 this.options.endDelim);
96 }
97
98 Impeller.isValidStrategy = function (strategy) {
99 return strategy === 'html' || strategy === 'js';
100 };
101
102 Impeller.mkAttrRegex = mkAttrRegex;
103
104 /**
105 * Replaces an original string with a translated one.
106 *
107 * @param {String} origString The phrase to be replaced
108 * @param {String} origNode The original node text; contains origString.
109 * In HTML, this is a tag.
110 * In JS, it's a string literal.
111 * @param {Object} jsonData Dictionary for searching
112 * @param {String} context The third argument of gettext as a key to
113 * choose which translation of the first argument
114 * to use for replacement
115 */
116 Impeller.prototype.translateReplace = function (origString, origNode,
117 jsonData, context, jsFuncName) {
118 origString = origString.trim();
119
120 if (typeof jsonData[origString] !== 'undefined') {
121 var newString = '';
122 if (context && typeof jsonData[origString] === 'object' &&
123 typeof jsonData[origString][context] !== 'undefined') {
124 newString = jsonData[origString][context];
125 } else {
126 newString = jsonData[origString];
127 }
128 var newNode = origNode.replace(origString, newString);
129 var origRegex;
130 if (jsFuncName) {
131 var escapedNode = origNode.replace(/([\^\$\.\|\?\*\+\(\)\[\{])/g, '\\$1');
132 origRegex = new RegExp(jsFuncName + ' *\\( *' + escapedNode + ' *\\)');
133 this.newSrc = this.newSrc.replace(origRegex, function () {
134 return jsFuncName + '(' + newNode + ')';
135 });
136 } else {
137 this.newSrc = this.newSrc.replace(origNode, newNode);
138 }
139 this.replaceCount++;
140 }
141 };
142
143 Impeller.prototype.translateTextNode = function (node, jsonData, context) {
144 var trimmedOriginal = node.data.trim();
145 var translation = jsonData[trimmedOriginal];
146 if (typeof translation !== 'undefined') {
147 var translatedString = '';
148 if (context && _typeof(translation) === 'object' &&
149 typeof translation[context] !== 'undefined') {
150 translatedString = translation[context];
151 } else {
152 translatedString = translation;
153 }
154 node.data = node.data.replace(trimmedOriginal, translatedString);
155 this.replaceCount++;
156 }
157 };
158
159 Impeller.prototype.translateInnerHTML = function (node, jsonData, context) {
160 var _this = this;
161 var trimmedHTML = node.html().trim();
162 var translation = jsonData[trimmedHTML];
163 if (typeof translation !== 'undefined') {
164 var translatedString = '';
165 if (context && _typeof(translation) === 'object' &&
166 typeof translation[context] !== 'undefined') {
167 translatedString = translation[context];
168 } else {
169 translatedString = translation;
170 }
171 node.html(node.html().replace(trimmedHTML, translatedString));
172 this.replaceCount++;
173 }
174 };
175
176 Impeller.prototype.replaceHtml = function (filename, src, jsonData) {
177 var replaceHtml = function (src) {
178 var $ = cheerio.load(src, {
179 decodeEntities: false,
180 withStartIndices: true
181 });
182 var _this = this;
183 var matches;
184 var newlines = function (index) {
185 return src.substr(0, index).match(/\n/g) || [];
186 };
187
188 $('*').each(function (index, n) {
189 var node = $(n);
190 var getAttr = function (attr) {
191 return node.attr(attr) || node.data(attr);
192 };
193 var str = node.html();
194 var extracted = {};
195 var possibleAttributes = _this.options.attributes;
196
197 possibleAttributes.forEach(function (attr) {
198 extracted[attr] = {
199 plural: getAttr(attr + '-plural'),
200 extractedComment: getAttr(attr + '-comment'),
201 context: getAttr(attr + '-context')
202 };
203 });
204
205 if (n.name === 'script') {
206 if (n.attribs.type === 'text/ng-template') {
207 replaceHtml(node.text(), newlines(n.startIndex).length);
208 return;
209 }
210
211 // In HTML5, type defaults to text/javascript.
212 // In HTML4, it's required, so if it's not there, just assume it's JS
213 if (!n.attribs.type || n.attribs.type === 'text/javascript') {
214 _this.replaceJs(filename, node.text(), jsonData,
215 newlines(n.startIndex).length);
216 return;
217 }
218 }
219
220 if (node.is('translate')) {
221 _this.translateInnerHTML(node, jsonData);
222 return;
223 }
224
225
226 var attrs = node.attr();
227 for (var attr in attrs) {
228 if (attrs.hasOwnProperty(attr)) {
229 attr = attr.replace(/^data-/, '');
230 if (possibleAttributes.indexOf(attr) > -1) {
231 var attrValue = extracted[attr];
232 // TODO: support plurals, comments
233 _this.translateInnerHTML(node, jsonData,
234 attrValue.context);
235 } else if (matches = noDelimRegex.exec(node.attr(attr))) {
236 str = matches[2].replace(/\\\'/g, '\'');
237 noDelimRegex.lastIndex = 0;
238 }
239 }
240 }
241 });
242
243 _this.newSrc = $.html();
244
245 while (matches = this.attrRegex.exec(src)) {
246 var str = matches[2].replace(/\\\'/g, '\'');
247 _this.translateReplace(str, matches[0], jsonData);
248 }
249 }.bind(this);
250
251 replaceHtml(src, 0);
252 };
253
254 Impeller.prototype.replaceJs =
255 function (filename, src, jsonData, lineNumber) {
256 // used for line number of JS in HTML <script> tags
257 lineNumber = lineNumber || 0;
258 var _this = this;
259 var syntax;
260 try {
261 syntax = espree.parse(src, {
262 tolerant: true,
263 range: true,
264 attachComment: true,
265 loc: true,
266 token: true,
267 ecmaFeatures: {
268 arrowFunctions: true,
269 blockBindings: true,
270 destructuring: true,
271 regexYFlag: true,
272 regexUFlag: true,
273 templateStrings: true,
274 binaryLiterals: true,
275 octalLiterals: true,
276 unicodeCodePointEscapes: true,
277 defaultParams: true,
278 restParams: true,
279 forOf: true,
280 objectLiteralComputedProperties: true,
281 objectLiteralShorthandMethods: true,
282 objectLiteralShorthandProperties: true,
283 objectLiteralDuplicateProperties: true,
284 generators: true,
285 spread: true,
286 classes: true,
287 modules: true,
288 jsx: true,
289 globalReturn: true
290 }
291 });
292 } catch (err) {
293 console.log("Error of parsing js file: ", filename);
294 return;
295 }
296
297 _this.newSrc = src;
298
299 function isGettext(node) {
300 return node !== null &&
301 node.type === 'CallExpression' &&
302 node.callee !== null &&
303 (_this.options.markerNames.indexOf(node.callee.name) > -1 || (
304 node.callee.property &&
305 _this.options.markerNames.indexOf(node.callee.property.name) > -1
306 )) &&
307 node.arguments !== null &&
308 node.arguments.length;
309 }
310
311 function isGetString(node) {
312 return node !== null &&
313 node.type === 'CallExpression' &&
314 node.callee !== null &&
315 node.callee.type === 'MemberExpression' &&
316 node.callee.object !== null && (
317 node.callee.object.name === 'gettextCatalog' || (
318 // also allow gettextCatalog calls on objects like
319 // this.gettextCatalog.getString()
320 node.callee.object.property &&
321 node.callee.object.property.name === 'gettextCatalog')) &&
322 node.callee.property !== null &&
323 node.callee.property.name === 'getString' &&
324 node.arguments !== null &&
325 node.arguments.length;
326 }
327
328 walkJs(syntax, function (node) {
329 var phrase, nodeStr, context;
330 if (isGettext(node) || isGetString(node)) {
331 phrase = getJSExpression(node.arguments[0]);
332 nodeStr = getJSExpressionString(node.arguments[0]);
333
334 if (node.arguments[2]) {
335 context = getJSExpression(node.arguments[2]);
336 }
337 _this.translateReplace(phrase, nodeStr, jsonData, context);
338 }
339 });
340 };
341
342 Impeller.prototype.isSupportedByStrategy = function (strategy, extension) {
343 return (extension in this.options.extensions) &&
344 (this.options.extensions[extension] === strategy);
345 };
346
347 Impeller.prototype.processFile = function (filename, content, jsonData) {
348 var extension = filename.split('.').pop();
349 delete this.newSrc;
350 this.replaceCount = 0;
351 if (this.isSupportedByStrategy('html', extension)) {
352 this.replaceHtml(filename, content, jsonData);
353 }
354 if (this.isSupportedByStrategy('js', extension)) {
355 this.replaceJs(filename, content, jsonData);
356 }
357 return this.replaceCount;
358 };
359
360 Impeller.prototype.toString = function () {
361 return this.newSrc
362 .replace(/ translate\=""/g, ' translate');
363 };
364
365 return Impeller;
366})();
367
368module.exports = Impeller;