The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

462 lines
15KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2013 - Raw Material Software Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. #ifndef __JUCER_TRANSLATIONTOOL_JUCEHEADER__
  18. #define __JUCER_TRANSLATIONTOOL_JUCEHEADER__
  19. struct TranslationHelpers
  20. {
  21. static void addString (StringArray& strings, const String& s)
  22. {
  23. if (s.isNotEmpty() && ! strings.contains (s))
  24. strings.add (s);
  25. }
  26. static void scanFileForTranslations (StringArray& strings, const File& file)
  27. {
  28. const String content (file.loadFileAsString());
  29. String::CharPointerType p (content.getCharPointer());
  30. for (;;)
  31. {
  32. p = CharacterFunctions::find (p, CharPointer_ASCII ("TRANS"));
  33. if (p.isEmpty())
  34. break;
  35. p += 5;
  36. p = p.findEndOfWhitespace();
  37. if (*p == '(')
  38. {
  39. ++p;
  40. MemoryOutputStream text;
  41. parseStringLiteral (p, text);
  42. addString (strings, text.toString());
  43. }
  44. }
  45. }
  46. static void parseStringLiteral (String::CharPointerType& p, MemoryOutputStream& out) noexcept
  47. {
  48. p = p.findEndOfWhitespace();
  49. if (p.getAndAdvance() == '"')
  50. {
  51. String::CharPointerType start (p);
  52. for (;;)
  53. {
  54. juce_wchar c = *p;
  55. if (c == '"')
  56. {
  57. out << String (start, p);
  58. ++p;
  59. parseStringLiteral (p, out);
  60. return;
  61. }
  62. if (c == 0)
  63. break;
  64. if (c == '\\')
  65. {
  66. out << String (start, p);
  67. ++p;
  68. out << String::charToString (readEscapedChar (p));
  69. start = p + 1;
  70. }
  71. ++p;
  72. }
  73. }
  74. }
  75. static juce_wchar readEscapedChar (String::CharPointerType& p)
  76. {
  77. juce_wchar c = *p;
  78. switch (c)
  79. {
  80. case '"':
  81. case '\\':
  82. case '/': break;
  83. case 'b': c = '\b'; break;
  84. case 'f': c = '\f'; break;
  85. case 'n': c = '\n'; break;
  86. case 'r': c = '\r'; break;
  87. case 't': c = '\t'; break;
  88. case 'x':
  89. ++p;
  90. c = 0;
  91. for (int i = 4; --i >= 0;)
  92. {
  93. const int digitValue = CharacterFunctions::getHexDigitValue (*p);
  94. if (digitValue < 0)
  95. break;
  96. ++p;
  97. c = (juce_wchar) ((c << 4) + digitValue);
  98. }
  99. break;
  100. case '0': case '1': case '2': case '3': case '4':
  101. case '5': case '6': case '7': case '8': case '9':
  102. c = 0;
  103. for (int i = 4; --i >= 0;)
  104. {
  105. const int digitValue = *p - '0';
  106. if (digitValue < 0 || digitValue > 7)
  107. break;
  108. ++p;
  109. c = (juce_wchar) ((c << 3) + digitValue);
  110. }
  111. break;
  112. default:
  113. break;
  114. }
  115. return c;
  116. }
  117. static void scanFilesForTranslations (StringArray& strings, const Project::Item& p)
  118. {
  119. if (p.isFile())
  120. {
  121. const File file (p.getFile());
  122. if (file.hasFileExtension (sourceOrHeaderFileExtensions))
  123. scanFileForTranslations (strings, file);
  124. }
  125. for (int i = 0; i < p.getNumChildren(); ++i)
  126. scanFilesForTranslations (strings, p.getChild (i));
  127. }
  128. static void scanProject (StringArray& strings, Project& project)
  129. {
  130. scanFilesForTranslations (strings, project.getMainGroup());
  131. OwnedArray<LibraryModule> modules;
  132. project.getModules().createRequiredModules (modules);
  133. for (int j = 0; j < modules.size(); ++j)
  134. {
  135. const File localFolder (modules.getUnchecked(j)->getFolder());
  136. Array<File> files;
  137. modules.getUnchecked(j)->findBrowseableFiles (localFolder, files);
  138. for (int i = 0; i < files.size(); ++i)
  139. scanFileForTranslations (strings, files.getReference(i));
  140. }
  141. }
  142. static const char* getMungingSeparator() { return "JCTRIDX"; }
  143. static StringArray breakApart (const String& munged)
  144. {
  145. StringArray lines, result;
  146. lines.addLines (munged);
  147. String currentItem;
  148. for (int i = 0; i < lines.size(); ++i)
  149. {
  150. if (lines[i].contains (getMungingSeparator()))
  151. {
  152. if (currentItem.isNotEmpty())
  153. result.add (currentItem);
  154. currentItem = String::empty;
  155. }
  156. else
  157. {
  158. if (currentItem.isNotEmpty())
  159. currentItem << newLine;
  160. currentItem << lines[i];
  161. }
  162. }
  163. if (currentItem.isNotEmpty())
  164. result.add (currentItem);
  165. return result;
  166. }
  167. static String escapeString (const String& s)
  168. {
  169. return s.replace ("\"", "\\\"")
  170. .replace ("\'", "\\\'")
  171. .replace ("\t", "\\t")
  172. .replace ("\r", "\\r")
  173. .replace ("\n", "\\n");
  174. }
  175. static String getPreTranslationText (Project& project)
  176. {
  177. StringArray strings;
  178. scanProject (strings, project);
  179. return mungeStrings (strings);
  180. }
  181. static String getPreTranslationText (const LocalisedStrings& strings)
  182. {
  183. return mungeStrings (strings.getMappings().getAllKeys());
  184. }
  185. static String mungeStrings (const StringArray& strings)
  186. {
  187. MemoryOutputStream s;
  188. for (int i = 0; i < strings.size(); ++i)
  189. {
  190. s << getMungingSeparator() << i << "." << newLine << strings[i];
  191. if (i < strings.size() - 1)
  192. s << newLine;
  193. }
  194. return s.toString();
  195. }
  196. static String createLine (const String& preString, const String& postString)
  197. {
  198. return "\"" + escapeString (preString)
  199. + "\" = \""
  200. + escapeString (postString) + "\"";
  201. }
  202. static String createFinishedTranslationFile (StringArray preStrings,
  203. StringArray postStrings,
  204. const LocalisedStrings& original)
  205. {
  206. const StringPairArray& originalStrings (original.getMappings());
  207. StringArray lines;
  208. if (originalStrings.size() > 0)
  209. {
  210. lines.add ("language: " + original.getLanguageName());
  211. lines.add ("countries: " + original.getCountryCodes().joinIntoString (" "));
  212. lines.add (String::empty);
  213. const StringArray& originalKeys (originalStrings.getAllKeys());
  214. const StringArray& originalValues (originalStrings.getAllValues());
  215. int numRemoved = 0;
  216. for (int i = preStrings.size(); --i >= 0;)
  217. {
  218. if (originalKeys.contains (preStrings[i]))
  219. {
  220. preStrings.remove (i);
  221. postStrings.remove (i);
  222. ++numRemoved;
  223. }
  224. }
  225. for (int i = 0; i < originalStrings.size(); ++i)
  226. lines.add (createLine (originalKeys[i], originalValues[i]));
  227. }
  228. else
  229. {
  230. lines.add ("language: [enter full name of the language here!]");
  231. lines.add ("countries: [enter list of 2-character country codes here!]");
  232. lines.add (String::empty);
  233. }
  234. for (int i = 0; i < preStrings.size(); ++i)
  235. lines.add (createLine (preStrings[i], postStrings[i]));
  236. return lines.joinIntoString (newLine);
  237. }
  238. };
  239. //==============================================================================
  240. class TranslationToolComponent : public Component,
  241. public ButtonListener
  242. {
  243. public:
  244. TranslationToolComponent()
  245. : editorOriginal (documentOriginal, nullptr),
  246. editorPre (documentPre, nullptr),
  247. editorPost (documentPost, nullptr),
  248. editorResult (documentResult, nullptr)
  249. {
  250. setLookAndFeel (&lf);
  251. instructionsLabel.setText (
  252. "This utility converts translation files to/from a format that can be passed to automatic translation tools."
  253. "\n\n"
  254. "First, choose whether to scan the current project for all TRANS() macros, or "
  255. "pick an existing translation file to load:", dontSendNotification);
  256. addAndMakeVisible (instructionsLabel);
  257. label1.setText ("..then copy-and-paste this annotated text into Google Translate or some other translator:", dontSendNotification);
  258. addAndMakeVisible (label1);
  259. label2.setText ("...then, take the translated result and paste it into the box below:", dontSendNotification);
  260. addAndMakeVisible (label2);
  261. label3.setText ("Finally, click the 'Generate' button, and a translation file will be created below. "
  262. "Remember to update its language code at the top!", dontSendNotification);
  263. addAndMakeVisible (label3);
  264. label4.setText ("If you load an existing file the already translated strings will be removed. Ensure this box is empty to create a fresh translation", dontSendNotification);
  265. addAndMakeVisible (label4);
  266. addAndMakeVisible (editorOriginal);
  267. addAndMakeVisible (editorPre);
  268. addAndMakeVisible (editorPost);
  269. addAndMakeVisible (editorResult);
  270. generateButton.setButtonText (TRANS("Generate"));
  271. addAndMakeVisible (generateButton);
  272. scanButton.setButtonText ("Scan Project for TRANS macros");
  273. addAndMakeVisible (scanButton);
  274. loadButton.setButtonText ("Load existing translation File...");
  275. addAndMakeVisible (loadButton);
  276. generateButton.addListener (this);
  277. scanButton.addListener (this);
  278. loadButton.addListener (this);
  279. }
  280. void paint (Graphics& g)
  281. {
  282. IntrojucerLookAndFeel::fillWithBackgroundTexture (*this, g);
  283. }
  284. void resized()
  285. {
  286. const int m = 6;
  287. const int textH = 44;
  288. const int extraH = (7 * textH);
  289. const int editorH = (getHeight() - extraH) / 4;
  290. Rectangle<int> r (getLocalBounds().withTrimmedBottom (m));
  291. instructionsLabel.setBounds (r.removeFromTop (textH * 2).reduced (m));
  292. r.removeFromTop (m);
  293. Rectangle<int> r2 (r.removeFromTop (textH - (2 * m)));
  294. scanButton.setBounds (r2.removeFromLeft (r.getWidth() / 2).reduced (m, 0));
  295. loadButton.setBounds (r2.reduced (m, 0));
  296. label1.setBounds (r.removeFromTop (textH).reduced (m));
  297. editorPre.setBounds (r.removeFromTop (editorH).reduced (m, 0));
  298. label2.setBounds (r.removeFromTop (textH).reduced (m));
  299. editorPost.setBounds (r.removeFromTop (editorH).reduced (m, 0));
  300. r2 = r.removeFromTop (textH);
  301. generateButton.setBounds (r2.removeFromRight (152).reduced (m));
  302. label3.setBounds (r2.reduced (m));
  303. editorResult.setBounds (r.removeFromTop (editorH).reduced (m, 0));
  304. label4.setBounds (r.removeFromTop (textH).reduced (m));
  305. editorOriginal.setBounds (r.reduced (m, 0));
  306. }
  307. private:
  308. CodeDocument documentOriginal, documentPre, documentPost, documentResult;
  309. CodeEditorComponent editorOriginal, editorPre, editorPost, editorResult;
  310. juce::Label label1, label2, label3, label4;
  311. juce::TextButton generateButton;
  312. juce::Label instructionsLabel;
  313. juce::TextButton scanButton;
  314. juce::TextButton loadButton;
  315. IntrojucerLookAndFeel lf;
  316. void buttonClicked (Button* b)
  317. {
  318. if (b == &generateButton) generate();
  319. else if (b == &loadButton) loadFile();
  320. else if (b == &scanButton) scanProject();
  321. }
  322. void generate()
  323. {
  324. StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent()));
  325. StringArray postStrings (TranslationHelpers::breakApart (documentPost.getAllContent()));
  326. if (postStrings.size() != preStrings.size())
  327. {
  328. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  329. TRANS("Error"),
  330. TRANS("The pre- and post-translation text doesn't match!\n\n"
  331. "Perhaps it got mangled by the translator?"));
  332. return;
  333. }
  334. const LocalisedStrings originalTranslation (documentOriginal.getAllContent(), false);
  335. documentResult.replaceAllContent (TranslationHelpers::createFinishedTranslationFile (preStrings, postStrings, originalTranslation));
  336. }
  337. void loadFile()
  338. {
  339. FileChooser fc ("Choose a translation file to load",
  340. File::nonexistent,
  341. "*");
  342. if (fc.browseForFileToOpen())
  343. {
  344. const LocalisedStrings loadedStrings (fc.getResult(), false);
  345. documentOriginal.replaceAllContent (fc.getResult().loadFileAsString().trim());
  346. setPreTranslationText (TranslationHelpers::getPreTranslationText (loadedStrings));
  347. }
  348. }
  349. void scanProject()
  350. {
  351. if (Project* project = IntrojucerApp::getApp().mainWindowList.getFrontmostProject())
  352. setPreTranslationText (TranslationHelpers::getPreTranslationText (*project));
  353. else
  354. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Translation Tool",
  355. "This will only work when you have a project open!");
  356. }
  357. void setPreTranslationText (const String& text)
  358. {
  359. documentPre.replaceAllContent (text);
  360. editorPre.grabKeyboardFocus();
  361. editorPre.selectAll();
  362. }
  363. };
  364. #endif // __JUCER_TRANSLATIONTOOL_JUCEHEADER__