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.

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