PageRenderTime 45ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/tools/crowdin/sync.js

https://github.com/oaeproject/3akai-ux
JavaScript | 206 lines | 99 code | 22 blank | 85 comment | 7 complexity | 4108c0a252630e495e02dafc5250749e MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. /*!
  2. * Copyright 2014 Apereo Foundation (AF) Licensed under the
  3. * Educational Community License, Version 2.0 (the "License"); you may
  4. * not use this file except in compliance with the License. You may
  5. * obtain a copy of the License at
  6. *
  7. * http://opensource.org/licenses/ECL-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing,
  10. * software distributed under the License is distributed on an "AS IS"
  11. * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
  12. * or implied. See the License for the specific language governing
  13. * permissions and limitations under the License.
  14. */
  15. var argv = require('optimist')
  16. .usage('Usage: $0 -a <crowdin API key>')
  17. .demand('a')
  18. .alias('a', 'apiKey')
  19. .describe('a', 'Crowdin API key')
  20. .demand('r')
  21. .alias('r', 'rootDir')
  22. .describe('r', 'Absolute path to the 3akai-ux root directory')
  23. .argv;
  24. var _ = require('underscore');
  25. var fs = require('fs');
  26. var propertiesParser = require('properties-parser');
  27. var readdirp = require('readdirp');
  28. var shelljs = require('shelljs');
  29. var util = require('util');
  30. // Extract the API key from the provided command line parameter
  31. // @see http://crowdin.net/project/apereo-oae/settings
  32. var apiKey = argv.apiKey;
  33. // Extract the 3akai-ux root directory
  34. var rootDir = argv.rootDir;
  35. var crowdinDir = rootDir + '/tools/crowdin';
  36. /**
  37. * Download the crowdin synchronization JAR file. If the file is already in place, it will
  38. * not be re-downloaded
  39. *
  40. * @param {Function} callback Standard callback function
  41. */
  42. var downloadCrowdinLibrary = function(callback) {
  43. // Check whether or not the JAR file is already in place
  44. var exists = fs.existsSync(crowdinDir + '/crowdin-cli.jar');
  45. if (exists) {
  46. callback();
  47. } else {
  48. // Download the JAR file
  49. console.log('Downloading crowdin JAR file');
  50. shelljs.exec(util.format('curl https://crowdin.net/downloads/crowdin-cli.jar > %s/crowdin-cli.jar', crowdinDir), {}, callback);
  51. }
  52. };
  53. /**
  54. * Generate the YAML file that will be used to communicate with the crowdin synchronization API
  55. * @see http://crowdin.net/page/cli-tool
  56. */
  57. var generateYaml = function() {
  58. // Read the YAML template file
  59. var data = fs.readFileSync(crowdinDir + '/crowdin.template', 'utf-8');
  60. // Parse the underscore.js template and render it with the provided API key and root directory
  61. var template = _.template(data);
  62. var yaml = template({
  63. 'apiKey': apiKey,
  64. 'rootDir': rootDir
  65. });
  66. // Save the YAML template file
  67. fs.writeFileSync(crowdinDir + '/crowdin.yaml', yaml, 'utf-8');
  68. console.log('Generated YAML file');
  69. };
  70. /**
  71. * Synchronize the translations in the current branch with the translations on crowdin.
  72. *
  73. * 1. Upload the latest keys to crowdin
  74. * 2. Upload the latest translations to crowdin
  75. * 3. Download the latest translations from crowdin
  76. *
  77. * @param {Function} callback Standard callback function
  78. */
  79. var synchronizeTranslations = function(callback) {
  80. // Upload the latest set of available i18n keys to crowdin. This will add new keys to the list of keys
  81. // to translate and will remove keys that have been removed in the codebase from the list of keys to
  82. // translate
  83. shelljs.exec(util.format('cd %s; java -jar crowdin-cli.jar upload sources', crowdinDir), {}, function() {
  84. // Upload the latest translations to crowdin. This will update any translations that have been updated
  85. // in the codebase directly on crowdin. This is done before downloading the translations from crowdin, as
  86. // core development will end up updating the translations directly most of the time, and we want to avoid
  87. // overriding those changes with the translations on crowdin
  88. shelljs.exec(util.format('cd %s; java -jar crowdin-cli.jar upload translations', crowdinDir), {}, function() {
  89. // Download the latest updated translations from crowdin
  90. shelljs.exec(util.format('cd %s; java -jar crowdin-cli.jar download', crowdinDir), {}, function() {
  91. callback();
  92. });
  93. });
  94. });
  95. };
  96. /**
  97. * Extract the paths of all the available bundles (.properties files) in the code base
  98. *
  99. * @param {Function} callback Standard callback function
  100. * @param {String[]} callback.bundles List of paths for the available bundles
  101. */
  102. var collectBundles = function(callback) {
  103. var bundles = [];
  104. // Loop through all available core and widget translation bundles and store their paths
  105. readdirp({ 'root': rootDir, 'fileFilter': '*.properties' }, function(entry) {
  106. bundles.push(entry.fullPath);
  107. }, function(err, res) {
  108. callback(bundles);
  109. });
  110. };
  111. /**
  112. * Remove the empty language bundles from the list of provided bundles
  113. *
  114. * @param {String[]} bundles List of paths for the available bundles
  115. * @return {String[]} Updated list of paths for the available bundles, excluding all empty bundles
  116. */
  117. var removeEmptyBundles = function(bundles) {
  118. // Keep track of all empty bundles
  119. var bundlesToRemove = [];
  120. _.each(bundles, function(bundle) {
  121. var i18nKeys = propertiesParser.read(bundle);
  122. // Verify if the bundle is empty by checking if there are any
  123. // non-empty tranlations in the bundle
  124. var isEmpty = true;
  125. _.each(i18nKeys, function(translation) {
  126. if (translation) {
  127. isEmpty = false;
  128. }
  129. });
  130. // Remove empty bundles from the file system
  131. if (isEmpty) {
  132. bundlesToRemove.push(bundle);
  133. fs.unlinkSync(bundle);
  134. console.log('Removed empty bundle: ' + bundle);
  135. }
  136. });
  137. // Remove the empty bundles from the list of bundles
  138. return _.difference(bundles, bundlesToRemove);
  139. };
  140. /**
  141. * Update the widget manifest files to reflect the available languages for each widget
  142. *
  143. * @param {String[]} bundles List of paths for the available bundles
  144. */
  145. var updateWidgetManifests = function(bundles) {
  146. // Loop through all widget manifests
  147. readdirp({ 'root': rootDir, 'fileFilter': 'manifest.json' }, function(entry) {
  148. // Load the widget manifest
  149. var widgetManifest = require(entry.fullPath);
  150. // Get the list of bundles with translations for this widget
  151. var widgetLanguages = {};
  152. _.each(bundles, function(bundle) {
  153. if (bundle.indexOf(entry.fullParentDir + '/') === 0) {
  154. var language = bundle.split('/').pop().split('.')[0];
  155. widgetLanguages[language] = 'bundles/' + language + '.properties';
  156. }
  157. });
  158. // Add the languages to the widget manifest file
  159. if (_.keys(widgetLanguages).length) {
  160. widgetManifest['i18n'] = widgetLanguages;
  161. // If no languages are available for the current widget, the i18n property
  162. // is removed from the manifest file
  163. } else {
  164. delete widgetManifest['i18n'];
  165. }
  166. // Re-publish the manifest file
  167. fs.writeFileSync(entry.fullPath, JSON.stringify(widgetManifest, null, 4) + '\n');
  168. }, function() {});
  169. };
  170. // 1. Download the crowdin JAR file
  171. downloadCrowdinLibrary(function() {
  172. // 2. Generate the YAML file
  173. generateYaml();
  174. // 3. Synchronize the translations in the current branch with the translations on crowdin
  175. synchronizeTranslations(function() {
  176. // 4. Collect the available language bundles
  177. collectBundles(function(bundles) {
  178. // 5. Remove the bundles that don't have any translations in them
  179. bundles = removeEmptyBundles(bundles);
  180. // 6. Update widget manifests to only include existing bundles
  181. updateWidgetManifests(bundles);
  182. });
  183. });
  184. });