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.

411 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. 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. const File modulesFolder (ModuleList::getDefaultModulesFolder (&project));
  131. OwnedArray<LibraryModule> modules;
  132. ModuleList moduleList;
  133. moduleList.rescan (modulesFolder);
  134. project.createRequiredModules (moduleList, modules);
  135. for (int j = 0; j < modules.size(); ++j)
  136. {
  137. const File localFolder (modules.getUnchecked(j)->getLocalFolderFor (project));
  138. Array<File> files;
  139. modules.getUnchecked(j)->findBrowseableFiles (localFolder, files);
  140. for (int i = 0; i < files.size(); ++i)
  141. scanFileForTranslations (strings, files.getReference(i));
  142. }
  143. }
  144. static const char* getMungingSeparator() { return "JCTRIDX"; }
  145. static StringArray breakApart (const String& munged)
  146. {
  147. StringArray lines, result;
  148. lines.addLines (munged);
  149. String currentItem;
  150. for (int i = 0; i < lines.size(); ++i)
  151. {
  152. if (lines[i].contains (getMungingSeparator()))
  153. {
  154. if (currentItem.isNotEmpty())
  155. result.add (currentItem);
  156. currentItem = String::empty;
  157. }
  158. else
  159. {
  160. if (currentItem.isNotEmpty())
  161. currentItem << newLine;
  162. currentItem << lines[i];
  163. }
  164. }
  165. if (currentItem.isNotEmpty())
  166. result.add (currentItem);
  167. return result;
  168. }
  169. static String escapeString (const String& s)
  170. {
  171. return s.replace ("\"", "\\\"")
  172. .replace ("\'", "\\\'")
  173. .replace ("\t", "\\t")
  174. .replace ("\r", "\\r")
  175. .replace ("\n", "\\n");
  176. }
  177. static String getPreTranslationText (Project& project)
  178. {
  179. StringArray strings;
  180. scanProject (strings, project);
  181. return mungeStrings (strings);
  182. }
  183. static String getPreTranslationText (const LocalisedStrings& strings)
  184. {
  185. return mungeStrings (strings.getMappings().getAllKeys());
  186. }
  187. static String mungeStrings (const StringArray& strings)
  188. {
  189. MemoryOutputStream s;
  190. for (int i = 0; i < strings.size(); ++i)
  191. {
  192. s << getMungingSeparator() << i << "." << newLine << strings[i];
  193. if (i < strings.size() - 1)
  194. s << newLine;
  195. }
  196. return s.toString();
  197. }
  198. static String createFinishedTranslationFile (const StringArray& preStrings,
  199. const StringArray& postStrings)
  200. {
  201. StringArray lines;
  202. lines.add ("language: [enter full name of the language here!]");
  203. lines.add ("countries: [enter list of 2-character country codes here!]");
  204. lines.add (String::empty);
  205. for (int i = 0; i < preStrings.size(); ++i)
  206. lines.add ("\"" + escapeString (preStrings[i])
  207. + "\" = \""
  208. + escapeString (postStrings[i]) + "\"");
  209. return lines.joinIntoString (newLine);
  210. }
  211. };
  212. //==============================================================================
  213. class TranslationToolComponent : public Component,
  214. public ButtonListener
  215. {
  216. public:
  217. TranslationToolComponent()
  218. : editorPre (documentPre, nullptr),
  219. editorPost (documentPost, nullptr),
  220. editorResult (documentResult, nullptr)
  221. {
  222. setLookAndFeel (&lf);
  223. instructionsLabel.setText (
  224. "This utility converts translation files to/from a format that can be passed to automatic translation tools."
  225. "\n\n"
  226. "First, choose whether to scan the current project for all TRANS() macros, or "
  227. "pick an existing translation file to load:", dontSendNotification);
  228. addAndMakeVisible (&instructionsLabel);
  229. label1.setText ("..then copy-and-paste this annotated text into Google Translate or some other translator:", dontSendNotification);
  230. addAndMakeVisible (&label1);
  231. label2.setText ("...then, take the translated result and paste it into the box below:", dontSendNotification);
  232. addAndMakeVisible (&label2);
  233. label3.setText ("Finally, click the 'Generate' button, and a translation file will be created below. "
  234. "Remember to update its language code at the top!", dontSendNotification);
  235. addAndMakeVisible (&label3);
  236. addAndMakeVisible (&editorPre);
  237. addAndMakeVisible (&editorPost);
  238. addAndMakeVisible (&editorResult);
  239. generateButton.setButtonText (TRANS("Generate"));
  240. addAndMakeVisible (&generateButton);
  241. scanButton.setButtonText ("Scan Project for TRANS macros");
  242. addAndMakeVisible (&scanButton);
  243. loadButton.setButtonText ("Load existing translation File...");
  244. addAndMakeVisible (&loadButton);
  245. generateButton.addListener (this);
  246. scanButton.addListener (this);
  247. loadButton.addListener (this);
  248. }
  249. void paint (Graphics& g)
  250. {
  251. IntrojucerLookAndFeel::fillWithBackgroundTexture (*this, g);
  252. }
  253. void resized()
  254. {
  255. Rectangle<int> r (getLocalBounds());
  256. r.removeFromTop (120);
  257. editorPre.setBounds (10, 165, getWidth() - 20, 130);
  258. editorPost.setBounds (10, 338, getWidth() - 20, 114);
  259. editorResult.setBounds (10, 503, getWidth() - 20, getHeight() - 510);
  260. generateButton.setBounds (getWidth() - 152, 462, 140, 30);
  261. label1.setBounds (10, 128, getWidth() - 20, 26);
  262. label2.setBounds (10, 303, getWidth() - 20, 25);
  263. label3.setBounds (10, 459, generateButton.getX() - 20, 38);
  264. instructionsLabel.setBounds (6, 10, getWidth() - 14, 70);
  265. scanButton.setBounds (27, 86, 257, 30);
  266. loadButton.setBounds (304, 86, 260, 30);
  267. }
  268. private:
  269. CodeDocument documentPre, documentPost, documentResult;
  270. CodeEditorComponent editorPre, editorPost, editorResult;
  271. juce::Label label1, label2, label3;
  272. juce::TextButton generateButton;
  273. juce::Label instructionsLabel;
  274. juce::TextButton scanButton;
  275. juce::TextButton loadButton;
  276. IntrojucerLookAndFeel lf;
  277. void buttonClicked (Button* b)
  278. {
  279. if (b == &generateButton) generate();
  280. else if (b == &loadButton) loadFile();
  281. else if (b == &scanButton) scanProject();
  282. }
  283. void generate()
  284. {
  285. StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent()));
  286. StringArray postStrings (TranslationHelpers::breakApart (documentPost.getAllContent()));
  287. if (postStrings.size() != preStrings.size())
  288. {
  289. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  290. TRANS("Error"),
  291. TRANS("The pre- and post-translation text doesn't match!\n\n"
  292. "Perhaps it got mangled by the translator?"));
  293. return;
  294. }
  295. documentResult.replaceAllContent (TranslationHelpers::createFinishedTranslationFile (preStrings, postStrings));
  296. }
  297. void loadFile()
  298. {
  299. FileChooser fc ("Choose a translation file to load",
  300. File::nonexistent,
  301. "*");
  302. if (fc.browseForFileToOpen())
  303. setPreTranslationText (TranslationHelpers::getPreTranslationText (LocalisedStrings (fc.getResult(), false)));
  304. }
  305. void scanProject()
  306. {
  307. if (Project* project = IntrojucerApp::getApp().mainWindowList.getFrontmostProject())
  308. setPreTranslationText (TranslationHelpers::getPreTranslationText (*project));
  309. else
  310. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Translation Tool",
  311. "This will only work when you have a project open!");
  312. }
  313. void setPreTranslationText (const String& text)
  314. {
  315. documentPre.replaceAllContent (text);
  316. editorPre.grabKeyboardFocus();
  317. editorPre.selectAll();
  318. }
  319. };
  320. #endif // __JUCER_TRANSLATIONTOOL_JUCEHEADER__