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.

457 lines
15KB

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