convertTranslations.js 8.9 KB

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