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.

408 lines
13KB

  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 createFinishedTranslationFile (const StringArray& preStrings,
  197. const StringArray& postStrings)
  198. {
  199. StringArray lines;
  200. lines.add ("language: [enter full name of the language here!]");
  201. lines.add ("countries: [enter list of 2-character country codes here!]");
  202. lines.add (String::empty);
  203. for (int i = 0; i < preStrings.size(); ++i)
  204. lines.add ("\"" + escapeString (preStrings[i])
  205. + "\" = \""
  206. + escapeString (postStrings[i]) + "\"");
  207. return lines.joinIntoString (newLine);
  208. }
  209. };
  210. //==============================================================================
  211. class TranslationToolComponent : public Component,
  212. public ButtonListener
  213. {
  214. public:
  215. TranslationToolComponent()
  216. : editorPre (documentPre, nullptr),
  217. editorPost (documentPost, nullptr),
  218. editorResult (documentResult, nullptr)
  219. {
  220. setLookAndFeel (&lf);
  221. instructionsLabel.setText (
  222. "This utility converts translation files to/from a format that can be passed to automatic translation tools."
  223. "\n\n"
  224. "First, choose whether to scan the current project for all TRANS() macros, or "
  225. "pick an existing translation file to load:", dontSendNotification);
  226. addAndMakeVisible (instructionsLabel);
  227. label1.setText ("..then copy-and-paste this annotated text into Google Translate or some other translator:", dontSendNotification);
  228. addAndMakeVisible (label1);
  229. label2.setText ("...then, take the translated result and paste it into the box below:", dontSendNotification);
  230. addAndMakeVisible (label2);
  231. label3.setText ("Finally, click the 'Generate' button, and a translation file will be created below. "
  232. "Remember to update its language code at the top!", dontSendNotification);
  233. addAndMakeVisible (label3);
  234. addAndMakeVisible (editorPre);
  235. addAndMakeVisible (editorPost);
  236. addAndMakeVisible (editorResult);
  237. generateButton.setButtonText (TRANS("Generate"));
  238. addAndMakeVisible (generateButton);
  239. scanButton.setButtonText ("Scan Project for TRANS macros");
  240. addAndMakeVisible (scanButton);
  241. loadButton.setButtonText ("Load existing translation File...");
  242. addAndMakeVisible (loadButton);
  243. generateButton.addListener (this);
  244. scanButton.addListener (this);
  245. loadButton.addListener (this);
  246. }
  247. void paint (Graphics& g)
  248. {
  249. IntrojucerLookAndFeel::fillWithBackgroundTexture (*this, g);
  250. }
  251. void resized()
  252. {
  253. Rectangle<int> r (getLocalBounds());
  254. r.removeFromTop (120);
  255. editorPre.setBounds (10, 165, getWidth() - 20, 130);
  256. editorPost.setBounds (10, 338, getWidth() - 20, 114);
  257. editorResult.setBounds (10, 503, getWidth() - 20, getHeight() - 510);
  258. generateButton.setBounds (getWidth() - 152, 462, 140, 30);
  259. label1.setBounds (10, 128, getWidth() - 20, 26);
  260. label2.setBounds (10, 303, getWidth() - 20, 25);
  261. label3.setBounds (10, 459, generateButton.getX() - 20, 38);
  262. instructionsLabel.setBounds (6, 10, getWidth() - 14, 70);
  263. scanButton.setBounds (27, 86, 257, 30);
  264. loadButton.setBounds (304, 86, 260, 30);
  265. }
  266. private:
  267. CodeDocument documentPre, documentPost, documentResult;
  268. CodeEditorComponent editorPre, editorPost, editorResult;
  269. juce::Label label1, label2, label3;
  270. juce::TextButton generateButton;
  271. juce::Label instructionsLabel;
  272. juce::TextButton scanButton;
  273. juce::TextButton loadButton;
  274. IntrojucerLookAndFeel lf;
  275. void buttonClicked (Button* b)
  276. {
  277. if (b == &generateButton) generate();
  278. else if (b == &loadButton) loadFile();
  279. else if (b == &scanButton) scanProject();
  280. }
  281. void generate()
  282. {
  283. StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent()));
  284. StringArray postStrings (TranslationHelpers::breakApart (documentPost.getAllContent()));
  285. if (postStrings.size() != preStrings.size())
  286. {
  287. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  288. TRANS("Error"),
  289. TRANS("The pre- and post-translation text doesn't match!\n\n"
  290. "Perhaps it got mangled by the translator?"));
  291. return;
  292. }
  293. documentResult.replaceAllContent (TranslationHelpers::createFinishedTranslationFile (preStrings, postStrings));
  294. }
  295. void loadFile()
  296. {
  297. FileChooser fc ("Choose a translation file to load",
  298. File::nonexistent,
  299. "*");
  300. if (fc.browseForFileToOpen())
  301. setPreTranslationText (TranslationHelpers::getPreTranslationText (LocalisedStrings (fc.getResult(), false)));
  302. }
  303. void scanProject()
  304. {
  305. if (Project* project = IntrojucerApp::getApp().mainWindowList.getFrontmostProject())
  306. setPreTranslationText (TranslationHelpers::getPreTranslationText (*project));
  307. else
  308. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Translation Tool",
  309. "This will only work when you have a project open!");
  310. }
  311. void setPreTranslationText (const String& text)
  312. {
  313. documentPre.replaceAllContent (text);
  314. editorPre.grabKeyboardFocus();
  315. editorPre.selectAll();
  316. }
  317. };
  318. #endif // __JUCER_TRANSLATIONTOOL_JUCEHEADER__