convertTranslations.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. var fs = require('fs');
  2. function parseAndroid(data) {
  3. const rgxKeyValue = /<string name="(.*)">(.*)<\/string>/;
  4. const rgxCommentBlock = /<!-- ?(.*?) ?-->/;
  5. const rgxCommentStart = /<!-- ?(.*)/;
  6. const rgxCommentEnd = /(.*?) ?-->/;
  7. const rgxPluralsStart = /<plurals name="(.*)">/;
  8. const rgxPluralsEnd = /\s<\/plurals>/
  9. let lines = data.trim().split('\n');
  10. let result = {
  11. parsed: [],
  12. parsedPlurals: new Map()
  13. };
  14. let multilineComment = false;
  15. let pluralsDefinitionKey = null;
  16. for (let line of lines) {
  17. let kv = line.match(rgxKeyValue);
  18. if (kv != null) {
  19. result.parsed.push([kv[1], kv[2].
  20. replace(/([^\\])(")/g, '$1\\$2').
  21. replace(/&quot;/g, '\\"').
  22. replace(/&lt;/g, '<').
  23. replace(/&gt;/g, '>').
  24. replace(/&amp;/g, '&').
  25. replace(/\$s/ig, '$@')])
  26. continue;
  27. }
  28. let blockComment = line.match(rgxCommentBlock);
  29. if (blockComment) {
  30. result.parsed.push(blockComment[1]);
  31. continue;
  32. }
  33. let commentStart = line.match(rgxCommentStart);
  34. if (commentStart && !pluralsDefinition) {
  35. result.parsed.push(commentStart[1]);
  36. multilineComment = true;
  37. continue;
  38. }
  39. if (multilineComment) {
  40. let commentEnd = line.match(rgxCommentEnd);
  41. if (commentEnd) {
  42. result.parsed[result.parsed.length - 1] += '\n' + commentEnd[1];
  43. multilineComment = false;
  44. } else {
  45. result.parsed[result.parsed.length - 1] += '\n' + line;
  46. }
  47. continue;
  48. }
  49. let pluralsStart = line.match(rgxPluralsStart);
  50. if (pluralsStart) {
  51. pluralsDefinitionKey = pluralsStart[1];
  52. result.parsedPlurals.set(pluralsDefinitionKey, [ ]);
  53. continue;
  54. }
  55. if (pluralsDefinitionKey) {
  56. let pluralsEnd = line.match(rgxPluralsEnd)
  57. if (pluralsEnd) {
  58. pluralsDefinitionKey = null
  59. continue;
  60. } else if (isEmpty(line)) {
  61. continue;
  62. } else {
  63. result.parsedPlurals.get(pluralsDefinitionKey).push(line);
  64. }
  65. }
  66. if (isEmpty(line))
  67. result.parsed.push('');
  68. }
  69. return result;
  70. }
  71. function isEmpty(line) {
  72. return /^\s*$/.test(line);
  73. }
  74. function toStringsDict(pluralsMap) {
  75. if (!pluralsMap || pluralsMap.length == 0) {
  76. return;
  77. }
  78. const rgxZero = /<item quantity="zero">(.*)<\/item>/;
  79. const rgxOne = /<item quantity="one">(.*)<\/item>/;
  80. const rgxTwo = /<item quantity="two">(.*)<\/item>/;
  81. const rgxFew = /<item quantity="few">(.*)<\/item>/;
  82. const rgxMany = /<item quantity="many">(.*)<\/item>/;
  83. const rgxOther = /<item quantity="other">(.*)<\/item>/;
  84. let out = '\<?xml version=\"1.0\" encoding=\"UTF-8\"?\>\n';
  85. out += '\<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"\>\n';
  86. out += '\<plist version="1.0"\>\n';
  87. out += '\<dict\>\n';
  88. for (keyValuePair of pluralsMap) {
  89. let key = keyValuePair[0];
  90. out += '\t\<key\>' + key + '\</key\>\n';
  91. out += '\t\<dict\>\n';
  92. out += '\t\t\<key\>NSStringLocalizedFormatKey\</key\>\n'
  93. out += '\t\t\<string\>%#@localized_format_key@\</string\>\n'
  94. out += '\t\t\<key\>localized_format_key\</key\>\n'
  95. out += '\t\t\<dict\>\n'
  96. out += '\t\t\t\<key\>NSStringFormatSpecTypeKey\</key\>\n'
  97. out += '\t\t\t\<string\>NSStringPluralRuleType\</string\>\n'
  98. out += '\t\t\t\<key\>NSStringFormatValueTypeKey\</key\>\n'
  99. out += '\t\t\t\<string\>d\</string\>\n'
  100. let lines = keyValuePair[1];
  101. let zero = lines.filter( value => value.match(rgxZero));
  102. let one = lines.filter( value => value.match(rgxOne));
  103. let two = lines.filter( value => value.match(rgxTwo));
  104. let few = lines.filter( value => value.match(rgxFew));
  105. let many = lines.filter( value => value.match(rgxMany));
  106. let other = lines.filter( value => value.match(rgxOther))
  107. if (zero.length > 0) {
  108. out += '\t\t\t\<key\>zero\</key\>\n';
  109. out += '\t\t\t\<string\>'+zero[0].match(rgxZero)[1]+'\</string\>\n';
  110. }
  111. if (one.length > 0) {
  112. out += '\t\t\t\<key\>one\</key\>\n';
  113. out += '\t\t\t\<string\>'+one[0].match(rgxOne)[1]+'\</string\>\n';
  114. }
  115. if (two.length > 0) {
  116. out += '\t\t\t\<key\>two\</key\>\n';
  117. out += '\t\t\t\<string\>'+two[0].match(rgxTwo)[1]+'\</string\>\n';
  118. }
  119. if (few.length > 0) {
  120. out += '\t\t\t\<key\>few\</key\>\n';
  121. out += '\t\t\t\<string\>'+few[0].match(rgxFew)[1]+'\</string\>\n';
  122. }
  123. if (many.length > 0) {
  124. out += '\t\t\t\<key\>many\</key\>\n';
  125. out += '\t\t\t\<string\>'+many[0].match(rgxMany)[1]+'\</string\>\n';
  126. }
  127. if (other.length > 0) {
  128. out += '\t\t\t\<key\>other\</key\>\n';
  129. out += '\t\t\t\<string\>'+other[0].match(rgxOther)[1]+'\</string\>\n';
  130. }
  131. out += '\t\t\</dict\>\n'
  132. out += '\t\</dict\>\n';
  133. }
  134. out += '\</dict\>\n';
  135. out += '\</plist\>\n';
  136. return out;
  137. }
  138. function toInfoPlistStrings(lines) {
  139. let out = '';
  140. for (let line of lines) {
  141. if (typeof line === 'string') {
  142. continue;
  143. } else {
  144. let key = line[0];
  145. if (!key.startsWith("InfoPlist_")) {
  146. continue;
  147. }
  148. key = key.replace('InfoPlist_', '');
  149. out += `${key} = "${line[1]}";\n`;
  150. }
  151. }
  152. return out;
  153. }
  154. function toLocalizableStrings(lines) {
  155. let out = '';
  156. for (let line of lines) {
  157. if (typeof line === 'string') {
  158. if (line === '') {
  159. out += '\n';
  160. continue;
  161. }
  162. if (/\n/.test(line))
  163. out += '/* ' + line + ' */';
  164. else
  165. out += '// ' + line;
  166. } else {
  167. let key = line[0];
  168. if (key.startsWith("InfoPlist_")) {
  169. continue;
  170. }
  171. out += `"${key}" = "${line[1]}";`;
  172. }
  173. out += '\n';
  174. }
  175. return out;
  176. }
  177. function merge(base, addendum){
  178. var out = [].concat(base).filter(value => {
  179. return value != null;
  180. });
  181. for(let i in addendum){
  182. add = true;
  183. for (let j in base) {
  184. if (base[j][0] != undefined &&
  185. addendum[i][0] != undefined &&
  186. base[j][0] === addendum[i][0]) {
  187. add = false;
  188. break;
  189. }
  190. }
  191. if (add) {
  192. out.push(addendum[i]);
  193. }
  194. }
  195. return out;
  196. }
  197. function mergePlurals(base, appendum) {
  198. for (keyValuePair of appendum) {
  199. let key = keyValuePair[0];
  200. if (base[key] === undefined) {
  201. base.set(key, keyValuePair[1]);
  202. }
  203. }
  204. return base;
  205. }
  206. function parseXMLAndAppend(allElements, stringsXML) {
  207. var text = fs.readFileSync(stringsXML, 'utf-8').toString();
  208. let result = parseAndroid(text)
  209. allElements.parsed = merge(allElements.parsed, result.parsed);
  210. allElements.parsedPlurals = mergePlurals(allElements.parsedPlurals, result.parsedPlurals);
  211. return allElements;
  212. }
  213. function convertAndroidToIOS(stringsXMLArray, appleStrings) {
  214. let allElements = {
  215. parsed: [],
  216. parsedPlurals: new Map()
  217. };
  218. for (entry of stringsXMLArray) {
  219. allElements = parseXMLAndAppend(allElements, entry)
  220. console.log("parsed " + allElements.parsed.length + " entries of " + entry + " for Localizable.strings and " + allElements.parsedPlurals.size + " entries for Localizable.stringsdict");
  221. }
  222. let iosFormatted = toLocalizableStrings(allElements.parsed);
  223. let iosFormattedInfoPlist = toInfoPlistStrings(allElements.parsed);
  224. let iosFormattedPlurals = toStringsDict(allElements.parsedPlurals);
  225. let localizableStrings = output + "/Localizable.strings";
  226. let infoPlistStrings = output + "/InfoPlist.strings";
  227. let stringsDict = output + "/Localizable.stringsdict";
  228. fs.writeFile(localizableStrings, iosFormatted, function (err) {
  229. if (err) {
  230. console.error("Error converting " + stringsXMLArray + " to " + localizableStrings);
  231. throw err;
  232. }
  233. });
  234. fs.writeFile(infoPlistStrings, iosFormattedInfoPlist, function (err) {
  235. if (err) {
  236. console.error("Error converting " + stringsXMLArray + " to " + infoPlistStrings);
  237. throw err;
  238. }
  239. });
  240. fs.writeFile(stringsDict, iosFormattedPlurals, function (err) {
  241. if (err) {
  242. console.error("Error converting " + stringsXMLArray + " to " + stringsDict);
  243. throw err;
  244. }
  245. });
  246. }
  247. if (process.argv.length < 4) {
  248. console.error('Too less arguments provided. \nExample:\n ' +
  249. "node convertTranslations.js stringsInputfile1.xml stringsInputfile2.xml stringsInputfileN.xml path/to/outputfolder");
  250. process.exit(1);
  251. }
  252. var input = process.argv.slice(2, process.argv.length - 1)
  253. var output = process.argv[process.argv.length - 1];
  254. convertAndroidToIOS(input, output)