/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2013 - Raw Material Software Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ #ifndef __JUCER_TRANSLATIONTOOL_JUCEHEADER__ #define __JUCER_TRANSLATIONTOOL_JUCEHEADER__ struct TranslationHelpers { static void addString (StringArray& strings, const String& s) { if (s.isNotEmpty() && ! strings.contains (s)) strings.add (s); } static void scanFileForTranslations (StringArray& strings, const File& file) { const String content (file.loadFileAsString()); String::CharPointerType p (content.getCharPointer()); for (;;) { p = CharacterFunctions::find (p, CharPointer_ASCII ("TRANS")); if (p.isEmpty()) break; p += 5; p = p.findEndOfWhitespace(); if (*p == '(') { ++p; MemoryOutputStream text; parseStringLiteral (p, text); addString (strings, text.toString()); } } } static void parseStringLiteral (String::CharPointerType& p, MemoryOutputStream& out) noexcept { p = p.findEndOfWhitespace(); if (p.getAndAdvance() == '"') { String::CharPointerType start (p); for (;;) { juce_wchar c = *p; if (c == '"') { out << String (start, p); ++p; parseStringLiteral (p, out); return; } if (c == 0) break; if (c == '\\') { out << String (start, p); ++p; out << String::charToString (readEscapedChar (p)); start = p + 1; } ++p; } } } static juce_wchar readEscapedChar (String::CharPointerType& p) { juce_wchar c = *p; switch (c) { case '"': case '\\': case '/': break; case 'b': c = '\b'; break; case 'f': c = '\f'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case 'x': ++p; c = 0; for (int i = 4; --i >= 0;) { const int digitValue = CharacterFunctions::getHexDigitValue (*p); if (digitValue < 0) break; ++p; c = (juce_wchar) ((c << 4) + digitValue); } break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': c = 0; for (int i = 4; --i >= 0;) { const int digitValue = *p - '0'; if (digitValue < 0 || digitValue > 7) break; ++p; c = (juce_wchar) ((c << 3) + digitValue); } break; default: break; } return c; } static void scanFilesForTranslations (StringArray& strings, const Project::Item& p) { if (p.isFile()) { const File file (p.getFile()); if (file.hasFileExtension (sourceOrHeaderFileExtensions)) scanFileForTranslations (strings, file); } for (int i = 0; i < p.getNumChildren(); ++i) scanFilesForTranslations (strings, p.getChild (i)); } static void scanProject (StringArray& strings, Project& project) { scanFilesForTranslations (strings, project.getMainGroup()); OwnedArray modules; project.getModules().createRequiredModules (modules); for (int j = 0; j < modules.size(); ++j) { const File localFolder (modules.getUnchecked(j)->getFolder()); Array files; modules.getUnchecked(j)->findBrowseableFiles (localFolder, files); for (int i = 0; i < files.size(); ++i) scanFileForTranslations (strings, files.getReference(i)); } } static const char* getMungingSeparator() { return "JCTRIDX"; } static StringArray breakApart (const String& munged) { StringArray lines, result; lines.addLines (munged); String currentItem; for (int i = 0; i < lines.size(); ++i) { if (lines[i].contains (getMungingSeparator())) { if (currentItem.isNotEmpty()) result.add (currentItem); currentItem = String::empty; } else { if (currentItem.isNotEmpty()) currentItem << newLine; currentItem << lines[i]; } } if (currentItem.isNotEmpty()) result.add (currentItem); return result; } static String escapeString (const String& s) { return s.replace ("\"", "\\\"") .replace ("\'", "\\\'") .replace ("\t", "\\t") .replace ("\r", "\\r") .replace ("\n", "\\n"); } static String getPreTranslationText (Project& project) { StringArray strings; scanProject (strings, project); return mungeStrings (strings); } static String getPreTranslationText (const LocalisedStrings& strings) { return mungeStrings (strings.getMappings().getAllKeys()); } static String mungeStrings (const StringArray& strings) { MemoryOutputStream s; for (int i = 0; i < strings.size(); ++i) { s << getMungingSeparator() << i << "." << newLine << strings[i]; if (i < strings.size() - 1) s << newLine; } return s.toString(); } static String createLine (const String& preString, const String& postString) { return "\"" + escapeString (preString) + "\" = \"" + escapeString (postString) + "\""; } static String createFinishedTranslationFile (StringArray preStrings, StringArray postStrings, const LocalisedStrings& original) { const StringPairArray& originalStrings (original.getMappings()); StringArray lines; if (originalStrings.size() > 0) { lines.add ("language: " + original.getLanguageName()); lines.add ("countries: " + original.getCountryCodes().joinIntoString (" ")); lines.add (String::empty); const StringArray& originalKeys (originalStrings.getAllKeys()); const StringArray& originalValues (originalStrings.getAllValues()); int numRemoved = 0; for (int i = preStrings.size(); --i >= 0;) { if (originalKeys.contains (preStrings[i])) { preStrings.remove (i); postStrings.remove (i); ++numRemoved; } } for (int i = 0; i < originalStrings.size(); ++i) lines.add (createLine (originalKeys[i], originalValues[i])); } else { lines.add ("language: [enter full name of the language here!]"); lines.add ("countries: [enter list of 2-character country codes here!]"); lines.add (String::empty); } for (int i = 0; i < preStrings.size(); ++i) lines.add (createLine (preStrings[i], postStrings[i])); return lines.joinIntoString (newLine); } }; //============================================================================== class TranslationToolComponent : public Component, public ButtonListener { public: TranslationToolComponent() : editorOriginal (documentOriginal, nullptr), editorPre (documentPre, nullptr), editorPost (documentPost, nullptr), editorResult (documentResult, nullptr) { setLookAndFeel (&lf); instructionsLabel.setText ( "This utility converts translation files to/from a format that can be passed to automatic translation tools." "\n\n" "First, choose whether to scan the current project for all TRANS() macros, or " "pick an existing translation file to load:", dontSendNotification); addAndMakeVisible (instructionsLabel); label1.setText ("..then copy-and-paste this annotated text into Google Translate or some other translator:", dontSendNotification); addAndMakeVisible (label1); label2.setText ("...then, take the translated result and paste it into the box below:", dontSendNotification); addAndMakeVisible (label2); label3.setText ("Finally, click the 'Generate' button, and a translation file will be created below. " "Remember to update its language code at the top!", dontSendNotification); addAndMakeVisible (label3); 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); addAndMakeVisible (label4); addAndMakeVisible (editorOriginal); addAndMakeVisible (editorPre); addAndMakeVisible (editorPost); addAndMakeVisible (editorResult); generateButton.setButtonText (TRANS("Generate")); addAndMakeVisible (generateButton); scanButton.setButtonText ("Scan Project for TRANS macros"); addAndMakeVisible (scanButton); loadButton.setButtonText ("Load existing translation File..."); addAndMakeVisible (loadButton); generateButton.addListener (this); scanButton.addListener (this); loadButton.addListener (this); } void paint (Graphics& g) { IntrojucerLookAndFeel::fillWithBackgroundTexture (*this, g); } void resized() { const int m = 6; const int textH = 44; const int extraH = (7 * textH); const int editorH = (getHeight() - extraH) / 4; Rectangle r (getLocalBounds().withTrimmedBottom (m)); instructionsLabel.setBounds (r.removeFromTop (textH * 2).reduced (m)); r.removeFromTop (m); Rectangle r2 (r.removeFromTop (textH - (2 * m))); scanButton.setBounds (r2.removeFromLeft (r.getWidth() / 2).reduced (m, 0)); loadButton.setBounds (r2.reduced (m, 0)); label1.setBounds (r.removeFromTop (textH).reduced (m)); editorPre.setBounds (r.removeFromTop (editorH).reduced (m, 0)); label2.setBounds (r.removeFromTop (textH).reduced (m)); editorPost.setBounds (r.removeFromTop (editorH).reduced (m, 0)); r2 = r.removeFromTop (textH); generateButton.setBounds (r2.removeFromRight (152).reduced (m)); label3.setBounds (r2.reduced (m)); editorResult.setBounds (r.removeFromTop (editorH).reduced (m, 0)); label4.setBounds (r.removeFromTop (textH).reduced (m)); editorOriginal.setBounds (r.reduced (m, 0)); } private: CodeDocument documentOriginal, documentPre, documentPost, documentResult; CodeEditorComponent editorOriginal, editorPre, editorPost, editorResult; juce::Label label1, label2, label3, label4; juce::TextButton generateButton; juce::Label instructionsLabel; juce::TextButton scanButton; juce::TextButton loadButton; IntrojucerLookAndFeel lf; void buttonClicked (Button* b) { if (b == &generateButton) generate(); else if (b == &loadButton) loadFile(); else if (b == &scanButton) scanProject(); } void generate() { StringArray preStrings (TranslationHelpers::breakApart (documentPre.getAllContent())); StringArray postStrings (TranslationHelpers::breakApart (documentPost.getAllContent())); if (postStrings.size() != preStrings.size()) { AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, TRANS("Error"), TRANS("The pre- and post-translation text doesn't match!\n\n" "Perhaps it got mangled by the translator?")); return; } const LocalisedStrings originalTranslation (documentOriginal.getAllContent(), false); documentResult.replaceAllContent (TranslationHelpers::createFinishedTranslationFile (preStrings, postStrings, originalTranslation)); } void loadFile() { FileChooser fc ("Choose a translation file to load", File::nonexistent, "*"); if (fc.browseForFileToOpen()) { const LocalisedStrings loadedStrings (fc.getResult(), false); documentOriginal.replaceAllContent (fc.getResult().loadFileAsString().trim()); setPreTranslationText (TranslationHelpers::getPreTranslationText (loadedStrings)); } } void scanProject() { if (Project* project = IntrojucerApp::getApp().mainWindowList.getFrontmostProject()) setPreTranslationText (TranslationHelpers::getPreTranslationText (*project)); else AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Translation Tool", "This will only work when you have a project open!"); } void setPreTranslationText (const String& text) { documentPre.replaceAllContent (text); editorPre.grabKeyboardFocus(); editorPre.selectAll(); } }; #endif // __JUCER_TRANSLATIONTOOL_JUCEHEADER__