Merge pull request #1784 from garyhgohoos/23593-2
[xtuple] / scripts / lib / build_dictionary.js
1 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, strict:true, trailing:true, white:true */
3 /*global X:true, Backbone:true, _:true, XM:true, XT:true */
4
5 if (typeof XT === 'undefined') {
6   XT = {};
7 }
8
9 (function () {
10   "use strict";
11
12   var _ = require("underscore"),
13     async = require("async"),
14     fs = require("fs"),
15     locale = require("../../lib/tools/source/locale"),
16     path = require("path"),
17     createQuery = function (strings, context, language) {
18       return "select xt.set_dictionary($$%@$$, '%@', '%@');"
19         .f(JSON.stringify(strings),
20           context || "_core_",
21           language || "en_US");
22     };
23
24   /**
25     Looks (by convention) in en/strings.js of the extension for the
26     English strings and asyncronously returns the sql command to put
27     that hash into the database.
28    */
29   exports.getDictionarySql = function (extension, callback) {
30     var isLibOrm = extension.indexOf("lib/orm") >= 0,
31       isApplicationCore = extension.indexOf("enyo-client") >= 0 &&
32         extension.indexOf("extension") < 0,
33       clientHash,
34       databaseHash,
35       filename;
36
37     if (!XT.stringsFor) {
38       XT.getLanguage = locale.getLanguage;
39       XT.stringsFor = locale.stringsFor;
40     }
41     if (isLibOrm) {
42       // smash the tools and enyo-x strings together into one query
43       clientHash = _.extend(
44         require(path.join(extension, "../enyo-x/source/en/strings.js")).language.strings,
45         require(path.join(extension, "../tools/source/en/strings.js")).language.strings
46       );
47       callback(null, createQuery(clientHash, "_framework_"));
48
49     } else if (isApplicationCore) {
50       // put the client strings into one query
51       // put the database strings into another query
52       clientHash = require(path.join(extension, "application/source/en/strings.js")).language.strings;
53       databaseHash = require(path.join(extension, "database/source/en/strings.js")).language.strings;
54       callback(null, createQuery(clientHash) + createQuery(databaseHash, "_database_"));
55
56     } else {
57       // return the extension strings if they exist
58       filename = path.join(extension, "client/en/strings.js");
59       fs.exists(filename, function (exists) {
60         if (exists) {
61           callback(null, createQuery(require(filename).language.strings,
62             path.basename(extension).replace("/", "")));
63         } else {
64           // no problem. Maybe there is just no strings file
65           callback(null, '');
66         }
67       });
68     }
69   };
70
71
72   //
73   // The below code supports importing and exporting of dictionaries.
74   // This functionality can be accessed through the command line via
75   // the ./scripts/export_database.js and ./scripts/import_database.js
76   // files.
77   //
78
79   var dataSource = require('../../node-datasource/lib/ext/datasource').dataSource;
80   var querystring = require("querystring");
81   var request = require("request");
82
83   // Ask Google
84   // note that if we haven't been given an API key then the control flow
85   // will still come through here, but we'll just return the empty string
86   // synchronously.
87   var autoTranslate = function (text, apiKey, destinationLang, callback) {
88     if (!apiKey || !destinationLang || !text) {
89       // the user doesn't want to autotranslate
90       callback(null, "");
91       return;
92     }
93
94     if (destinationLang.indexOf("zh") === 0) {
95       // Google uses the country code for chinese, but uses dashes instead of our underscores
96       destinationLang = destinationLang.replace("_", "-");
97
98     } else if (destinationLang.indexOf("_") >= 0) {
99       // strip off the locale for google
100       destinationLang = destinationLang.substring(0, destinationLang.indexOf("_"));
101     }
102
103     var query = {
104         source: "en",
105         target: destinationLang,
106         key: apiKey,
107         q: text
108       },
109       url = "https://www.googleapis.com/language/translate/v2?" + querystring.stringify(query);
110
111     request.get(url, function (err, resp, body) {
112       if (err) {
113         callback(err);
114         return;
115       }
116       var response = JSON.parse(body);
117       if (response.error) {
118         callback(response.error);
119         return;
120       }
121       var translations = response.data.translations;
122       if (translations.length !== 1 || !translations[0].translatedText) {
123         console.log("could not parse translations", JSON.stringify(translations));
124         callback(null, "");
125         return;
126       }
127       var translation = translations[0].translatedText;
128       callback(null, translation);
129     });
130
131   };
132
133   //
134   // Group similar english and foreign rows together
135   // This will help us generate a dictionary file if there's
136   // already a partway- or fully- implemented translation already
137   // sitting in the database.
138   //
139   var marryLists = function (list) {
140     var englishList = _.filter(list, function (row) {
141       return row.dict_language_name === "en_US";
142     });
143     var foreignList = _.difference(list, englishList);
144     var marriedList = _.map(englishList, function (englishRow) {
145       var foreignRow = _.find(foreignList, function (foreignRow) {
146         return foreignRow.ext_name === englishRow.ext_name &&
147           foreignRow.dict_is_database === englishRow.dict_is_database &&
148           foreignRow.dict_is_framework === englishRow.dict_is_framework;
149       });
150       return {
151         source: englishRow,
152         target: foreignRow
153       };
154     });
155     return marriedList;
156   };
157
158   /**
159     @param {String} database. The database name, such as "dev"
160     @param {String} apiKey. Your Google Translate API key. Leave blank for no autotranslation
161     @param {String} destinationLang. In form "es_MX".
162     @param {Function} masterCallback
163    */
164   exports.exportEnglish = function (options, masterCallback) {
165     var creds = require("../../node-datasource/config").databaseServer,
166       sql = "select dict_strings, dict_is_database, dict_is_framework, " +
167         "dict_language_name, ext_name from xt.dict " +
168         "left join xt.ext on dict_ext_id = ext_id " +
169         "where dict_language_name = 'en_US'",
170       database = options.database,
171       apiKey = options.apiKey,
172       destinationDir = options.directory,
173       destinationLang = options.language;
174
175     if (destinationLang) {
176       sql = sql + " or dict_language_name = $1";
177       creds.parameters = [destinationLang];
178     } // else the user wants a blank template, so no need to search for pre-existing translations
179     sql = sql + ";";
180
181     creds.database = database;
182     dataSource.query(sql, creds, function (err, res) {
183       var processExtension = function (rowMap, extensionCallback) {
184         var row = rowMap.source;
185         var foreignStrings = rowMap.target ? JSON.parse(rowMap.target.dict_strings) : [];
186         var stringsArray = _.map(JSON.parse(row.dict_strings), function (value, key) {
187           return {value: value, key: key};
188         });
189         var processString = function (stringObj, stringCallback) {
190           //
191           // If this translation has already been made into the target language, put that
192           // translation into the dictionary file and do not bother autotranslating.
193           //
194           var preExistingTranslation = _.find(foreignStrings, function (foreignString, foreignKey) {
195             return foreignString && foreignKey === stringObj.key;
196           });
197           if (preExistingTranslation) {
198             // this has already been translated. No need to talk to Google etc.
199             stringCallback(null, {
200               key: stringObj.key,
201               source: stringObj.value,
202               target: preExistingTranslation
203             });
204           } else if ( destinationLang.indexOf('en') === 0 ) {
205              // if locale is en_AU en_GB copy the en_US source: strings to target:
206              stringCallback(null, {
207                key: stringObj.key,
208                source: stringObj.value,
209                target: stringObj.value
210              });
211           } else {
212             // ask google (or not)
213             autoTranslate(stringObj.value, apiKey, destinationLang, function (err, target) {
214               stringCallback(null, {
215                 key: stringObj.key,
216                 source: stringObj.value,
217                 target: target
218               });
219             });
220          }
221         };
222         async.map(stringsArray, processString, function (err, strings) {
223           extensionCallback(null, {
224             extension: row.dict_is_database ? "_database_" :
225               row.dict_is_framework ? "_framework_" :
226               row.ext_name || "_core_",
227             strings: strings
228           });
229         });
230       };
231
232       // group together english and foreign strings of the same extension
233       var marriedRows = marryLists(res.rows);
234       async.map(marriedRows, processExtension, function (err, extensions) {
235         // sort alpha so as to keep diffs under control
236         _.each(extensions, function (extension) {
237           extension.strings = _.sortBy(extension.strings, function (stringObj) {
238             return stringObj.key.toLowerCase();
239           });
240         });
241         extensions = _.sortBy(extensions, function (extObj) {
242           return extObj.extension;
243         });
244
245         var output = {
246           language: destinationLang || "",
247           extensions: extensions
248         };
249         // filename convention is ./scripts/output/es_MX_dictionary.js
250         destinationDir = destinationDir || path.join(__dirname, "../output");
251
252         var exportFilename = path.join(destinationDir,
253           (destinationLang || "blank") + "_dictionary.js");
254         console.log("Exporting to", exportFilename);
255         fs.writeFile(exportFilename, JSON.stringify(output, undefined, 2), function (err, result) {
256           masterCallback(err, result);
257         });
258       });
259     });
260   };
261
262   /**
263     Takes a dictionary definition file and inserts the data into the database
264    */
265   var importDictionary = exports.importDictionary = function (database, filename, masterCallback) {
266     var creds = require("../../node-datasource/config").databaseServer;
267     creds.database = database;
268
269     filename = path.resolve(process.cwd(), filename);
270     if (path.extname(filename) !== '.js') {
271       console.log("Skipping non-dictionary file", filename);
272       masterCallback();
273       return;
274     }
275     fs.readFile(filename, "utf8", function (err, contents) {
276       if (err) {
277         masterCallback(err);
278         return;
279       }
280       var dictionary = JSON.parse(contents);
281       var processExtension = function (extension, extensionCallback) {
282         var context = extension.extension;
283         var strings = _.reduce(extension.strings, function (memo, trans) {
284           memo[trans.key] = trans.target;
285           return memo;
286         }, {});
287         var sql = createQuery(strings, context, dictionary.language);
288
289         dataSource.query(sql, creds, function (err, res) {
290           extensionCallback(err, res);
291         });
292       };
293       async.each(dictionary.extensions, processExtension, function (err, results) {
294         masterCallback(err, results);
295       });
296     });
297   };
298
299   exports.importAllDictionaries = function (database, callback) {
300     var translationsDir = path.join(__dirname, "../../node_modules/xtuple-linguist/translations");
301     if (!fs.existsSync(translationsDir)) {
302       console.log("No translations directory found. Ignoring linguist.");
303       return callback();
304     }
305     var importOne = function (dictionary, next) {
306       importDictionary(database, dictionary, next);
307     };
308     var allDictionaries = _.map(fs.readdirSync(translationsDir), function (filename) {
309       return path.join(translationsDir, filename);
310     });
311     async.map(allDictionaries, importOne, callback);
312   };
313
314
315 }());