Google Docs Editor add-on quickstart

This quickstart creates a Google Docs Editor add-on that translates selected text in a document.

Objectives

  • Set up the script.
  • Run the script.

Prerequisites

To use this sample, you need the following prerequisites:

  • A Google Account (Google Workspace accounts might require administrator approval).
  • A web browser with access to the internet.

Set up the script

  1. Create a Google Docs document at docs.new .
  2. Click Extensions > Apps Script.
  3. Click Untitled project.
  4. Rename the Apps Script project Translate Docsand click Rename.
  5. Next to the Code.gs file, click More > Rename. Name the file translate .
  6. Click Add a file > HTML. Name the file sidebar .
  7. Replace the contents of each file with the following corresponding code, then click SaveSave icon.

    translate.gs

    docs/translate/translate.gs
     /** 
     * @OnlyCurrentDoc 
     * 
     * The above comment directs Apps Script to limit the scope of file 
     * access for this add-on. It specifies that this add-on will only 
     * attempt to read or modify the files in which the add-on is used, 
     * and not all of the user's files. The authorization request message 
     * presented to users will reflect this limited scope. 
     */ 
     /** 
     * Creates a menu entry in the Google Docs UI when the document is opened. 
     * This method is only used by the regular add-on, and is never called by 
     * the mobile add-on version. 
     * 
     * @param {object} e The event parameter for a simple onOpen trigger. To 
     *     determine which authorization mode (ScriptApp.AuthMode) the trigger is 
     *     running in, inspect e.authMode. 
     */ 
     function 
      
     onOpen 
     ( 
     e 
     ) 
      
     { 
      
     DocumentApp 
     . 
     getUi 
     (). 
     createAddonMenu 
     () 
      
     . 
     addItem 
     ( 
     'Start' 
     , 
      
     'showSidebar' 
     ) 
      
     . 
     addToUi 
     (); 
     } 
     /** 
     * Runs when the add-on is installed. 
     * This method is only used by the regular add-on, and is never called by 
     * the mobile add-on version. 
     * 
     * @param {object} e The event parameter for a simple onInstall trigger. To 
     *     determine which authorization mode (ScriptApp.AuthMode) the trigger is 
     *     running in, inspect e.authMode. (In practice, onInstall triggers always 
     *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or 
     *     AuthMode.NONE.) 
     */ 
     function 
      
     onInstall 
     ( 
     e 
     ) 
      
     { 
      
     onOpen 
     ( 
     e 
     ); 
     } 
     /** 
     * Opens a sidebar in the document containing the add-on's user interface. 
     * This method is only used by the regular add-on, and is never called by 
     * the mobile add-on version. 
     */ 
     function 
      
     showSidebar 
     () 
      
     { 
      
     const 
      
     ui 
      
     = 
      
     HtmlService 
     . 
     createHtmlOutputFromFile 
     ( 
     'sidebar' 
     ) 
      
     . 
     setTitle 
     ( 
     'Translate' 
     ); 
      
     DocumentApp 
     . 
     getUi 
     (). 
     showSidebar 
     ( 
     ui 
     ); 
     } 
     /** 
     * Gets the text the user has selected. If there is no selection, 
     * this function displays an error message. 
     * 
     * @return {Array.<string>} The selected text. 
     */ 
     function 
      
     getSelectedText 
     () 
      
     { 
      
     const 
      
     selection 
      
     = 
      
     DocumentApp 
     . 
     getActiveDocument 
     (). 
     getSelection 
     (); 
      
     const 
      
     text 
      
     = 
      
     []; 
      
     if 
      
     ( 
     selection 
     ) 
      
     { 
      
     const 
      
     elements 
      
     = 
      
     selection 
     . 
     getSelectedElements 
     (); 
      
     for 
      
     ( 
     let 
      
     i 
      
     = 
      
     0 
     ; 
      
     i 
     < 
     elements 
     . 
     length 
     ; 
      
     ++ 
     i 
     ) 
      
     { 
      
     if 
      
     ( 
     elements 
     [ 
     i 
     ]. 
     isPartial 
     ()) 
      
     { 
      
     const 
      
     element 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getElement 
     (). 
     asText 
     (); 
      
     const 
      
     startIndex 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getStartOffset 
     (); 
      
     const 
      
     endIndex 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getEndOffsetInclusive 
     (); 
      
     text 
     . 
     push 
     ( 
     element 
     . 
     getText 
     (). 
     substring 
     ( 
     startIndex 
     , 
      
     endIndex 
      
     + 
      
     1 
     )); 
      
     } 
      
     else 
      
     { 
      
     const 
      
     element 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getElement 
     (); 
      
     // Only translate elements that can be edited as text; skip images and 
      
     // other non-text elements. 
      
     if 
      
     ( 
     element 
     . 
     editAsText 
     ) 
      
     { 
      
     const 
      
     elementText 
      
     = 
      
     element 
     . 
     asText 
     (). 
     getText 
     (); 
      
     // This check is necessary to exclude images, which return a blank 
      
     // text element. 
      
     if 
      
     ( 
     elementText 
     ) 
      
     { 
      
     text 
     . 
     push 
     ( 
     elementText 
     ); 
      
     } 
      
     } 
      
     } 
      
     } 
      
     } 
      
     if 
      
     ( 
     ! 
     text 
     . 
     length 
     ) 
      
     throw 
      
     new 
      
     Error 
     ( 
     'Please select some text.' 
     ); 
      
     return 
      
     text 
     ; 
     } 
     /** 
     * Gets the stored user preferences for the origin and destination languages, 
     * if they exist. 
     * This method is only used by the regular add-on, and is never called by 
     * the mobile add-on version. 
     * 
     * @return {Object} The user's origin and destination language preferences, if 
     *     they exist. 
     */ 
     function 
      
     getPreferences 
     () 
      
     { 
      
     const 
      
     userProperties 
      
     = 
      
     PropertiesService 
     . 
     getUserProperties 
     (); 
      
     return 
      
     { 
      
     originLang 
     : 
      
     userProperties 
     . 
     getProperty 
     ( 
     'originLang' 
     ), 
      
     destLang 
     : 
      
     userProperties 
     . 
     getProperty 
     ( 
     'destLang' 
     ) 
      
     }; 
     } 
     /** 
     * Gets the user-selected text and translates it from the origin language to the 
     * destination language. The languages are notated by their two-letter short 
     * form. For example, English is 'en', and Spanish is 'es'. The origin language 
     * may be specified as an empty string to indicate that Google Translate should 
     * auto-detect the language. 
     * 
     * @param {string} origin The two-letter short form for the origin language. 
     * @param {string} dest The two-letter short form for the destination language. 
     * @param {boolean} savePrefs Whether to save the origin and destination 
     *     language preferences. 
     * @return {Object} Object containing the original text and the result of the 
     *     translation. 
     */ 
     function 
      
     getTextAndTranslation 
     ( 
     origin 
     , 
      
     dest 
     , 
      
     savePrefs 
     ) 
      
     { 
      
     if 
      
     ( 
     savePrefs 
     ) 
      
     { 
      
     PropertiesService 
     . 
     getUserProperties 
     () 
      
     . 
     setProperty 
     ( 
     'originLang' 
     , 
      
     origin 
     ) 
      
     . 
     setProperty 
     ( 
     'destLang' 
     , 
      
     dest 
     ); 
      
     } 
      
     const 
      
     text 
      
     = 
      
     getSelectedText 
     (). 
     join 
     ( 
     '\n' 
     ); 
      
     return 
      
     { 
      
     text 
     : 
      
     text 
     , 
      
     translation 
     : 
      
     translateText 
     ( 
     text 
     , 
      
     origin 
     , 
      
     dest 
     ) 
      
     }; 
     } 
     /** 
     * Replaces the text of the current selection with the provided text, or 
     * inserts text at the current cursor location. (There will always be either 
     * a selection or a cursor.) If multiple elements are selected, only inserts the 
     * translated text in the first element that can contain text and removes the 
     * other elements. 
     * 
     * @param {string} newText The text with which to replace the current selection. 
     */ 
     function 
      
     insertText 
     ( 
     newText 
     ) 
      
     { 
      
     const 
      
     selection 
      
     = 
      
     DocumentApp 
     . 
     getActiveDocument 
     (). 
     getSelection 
     (); 
      
     if 
      
     ( 
     selection 
     ) 
      
     { 
      
     let 
      
     replaced 
      
     = 
      
     false 
     ; 
      
     const 
      
     elements 
      
     = 
      
     selection 
     . 
     getSelectedElements 
     (); 
      
     if 
      
     ( 
     elements 
     . 
     length 
      
     === 
      
     1 
     && 
     elements 
     [ 
     0 
     ]. 
     getElement 
     (). 
     getType 
     () 
      
     === 
      
     DocumentApp 
     . 
     ElementType 
     . 
     INLINE_IMAGE 
     ) 
      
     { 
      
     throw 
      
     new 
      
     Error 
     ( 
     'Can\'t insert text into an image.' 
     ); 
      
     } 
      
     for 
      
     ( 
     let 
      
     i 
      
     = 
      
     0 
     ; 
      
     i 
     < 
     elements 
     . 
     length 
     ; 
      
     ++ 
     i 
     ) 
      
     { 
      
     if 
      
     ( 
     elements 
     [ 
     i 
     ]. 
     isPartial 
     ()) 
      
     { 
      
     const 
      
     element 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getElement 
     (). 
     asText 
     (); 
      
     const 
      
     startIndex 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getStartOffset 
     (); 
      
     const 
      
     endIndex 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getEndOffsetInclusive 
     (); 
      
     element 
     . 
     deleteText 
     ( 
     startIndex 
     , 
      
     endIndex 
     ); 
      
     if 
      
     ( 
     ! 
     replaced 
     ) 
      
     { 
      
     element 
     . 
     insertText 
     ( 
     startIndex 
     , 
      
     newText 
     ); 
      
     replaced 
      
     = 
      
     true 
     ; 
      
     } 
      
     else 
      
     { 
      
     // This block handles a selection that ends with a partial element. We 
      
     // want to copy this partial text to the previous element so we don't 
      
     // have a line-break before the last partial. 
      
     const 
      
     parent 
      
     = 
      
     element 
     . 
     getParent 
     (); 
      
     const 
      
     remainingText 
      
     = 
      
     element 
     . 
     getText 
     (). 
     substring 
     ( 
     endIndex 
      
     + 
      
     1 
     ); 
      
     parent 
     . 
     getPreviousSibling 
     (). 
     asText 
     (). 
     appendText 
     ( 
     remainingText 
     ); 
      
     // We cannot remove the last paragraph of a doc. If this is the case, 
      
     // just remove the text within the last paragraph instead. 
      
     if 
      
     ( 
     parent 
     . 
     getNextSibling 
     ()) 
      
     { 
      
     parent 
     . 
     removeFromParent 
     (); 
      
     } 
      
     else 
      
     { 
      
     element 
     . 
     removeFromParent 
     (); 
      
     } 
      
     } 
      
     } 
      
     else 
      
     { 
      
     const 
      
     element 
      
     = 
      
     elements 
     [ 
     i 
     ]. 
     getElement 
     (); 
      
     if 
      
     ( 
     ! 
     replaced 
     && 
     element 
     . 
     editAsText 
     ) 
      
     { 
      
     // Only translate elements that can be edited as text, removing other 
      
     // elements. 
      
     element 
     . 
     clear 
     (); 
      
     element 
     . 
     asText 
     (). 
     setText 
     ( 
     newText 
     ); 
      
     replaced 
      
     = 
      
     true 
     ; 
      
     } 
      
     else 
      
     { 
      
     // We cannot remove the last paragraph of a doc. If this is the case, 
      
     // just clear the element. 
      
     if 
      
     ( 
     element 
     . 
     getNextSibling 
     ()) 
      
     { 
      
     element 
     . 
     removeFromParent 
     (); 
      
     } 
      
     else 
      
     { 
      
     element 
     . 
     clear 
     (); 
      
     } 
      
     } 
      
     } 
      
     } 
      
     } 
      
     else 
      
     { 
      
     const 
      
     cursor 
      
     = 
      
     DocumentApp 
     . 
     getActiveDocument 
     (). 
     getCursor 
     (); 
      
     const 
      
     surroundingText 
      
     = 
      
     cursor 
     . 
     getSurroundingText 
     (). 
     getText 
     (); 
      
     const 
      
     surroundingTextOffset 
      
     = 
      
     cursor 
     . 
     getSurroundingTextOffset 
     (); 
      
     // If the cursor follows or preceds a non-space character, insert a space 
      
     // between the character and the translation. Otherwise, just insert the 
      
     // translation. 
      
     if 
      
     ( 
     surroundingTextOffset 
     > 
     0 
     ) 
      
     { 
      
     if 
      
     ( 
     surroundingText 
     . 
     charAt 
     ( 
     surroundingTextOffset 
      
     - 
      
     1 
     ) 
      
     !== 
      
     ' ' 
     ) 
      
     { 
      
     newText 
      
     = 
      
     ' ' 
      
     + 
      
     newText 
     ; 
      
     } 
      
     } 
      
     if 
      
     ( 
     surroundingTextOffset 
     < 
     surroundingText 
     . 
     length 
     ) 
      
     { 
      
     if 
      
     ( 
     surroundingText 
     . 
     charAt 
     ( 
     surroundingTextOffset 
     ) 
      
     !== 
      
     ' ' 
     ) 
      
     { 
      
     newText 
      
     += 
      
     ' ' 
     ; 
      
     } 
      
     } 
      
     cursor 
     . 
     insertText 
     ( 
     newText 
     ); 
      
     } 
     } 
     /** 
     * Given text, translate it from the origin language to the destination 
     * language. The languages are notated by their two-letter short form. For 
     * example, English is 'en', and Spanish is 'es'. The origin language may be 
     * specified as an empty string to indicate that Google Translate should 
     * auto-detect the language. 
     * 
     * @param {string} text text to translate. 
     * @param {string} origin The two-letter short form for the origin language. 
     * @param {string} dest The two-letter short form for the destination language. 
     * @return {string} The result of the translation, or the original text if 
     *     origin and dest languages are the same. 
     */ 
     function 
      
     translateText 
     ( 
     text 
     , 
      
     origin 
     , 
      
     dest 
     ) 
      
     { 
      
     if 
      
     ( 
     origin 
      
     === 
      
     dest 
     ) 
      
     return 
      
     text 
     ; 
      
     return 
      
     LanguageApp 
     . 
     translate 
     ( 
     text 
     , 
      
     origin 
     , 
      
     dest 
     ); 
     } 
    

    sidebar.html

    docs/translate/sidebar.html
    <!DOCTYPE html>
    <html>
    <head>
      <base target="_top">
      <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
      <!-- The CSS package above applies Google styling to buttons and other elements. -->
    
      <style>
        .branding-below {
          bottom: 56px;
          top: 0;
        }
        .branding-text {
          left: 7px;
          position: relative;
          top: 3px;
        }
        .col-contain {
          overflow: hidden;
        }
        .col-one {
          float: left;
          width: 50%;
        }
        .logo {
          vertical-align: middle;
        }
        .radio-spacer {
          height: 20px;
        }
        .width-100 {
          width: 100%;
        }
      </style>
      <title></title>
    </head>
    <body>
    <div class="sidebar branding-below">
      <form>
        <div class="block col-contain">
          <div class="col-one">
            <b>Selected text</b>
            <div>
              <input type="radio" name="origin" id="radio-origin-auto" value="" checked="checked">
              <label for="radio-origin-auto">Auto-detect</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-en" value="en">
              <label for="radio-origin-en">English</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-fr" value="fr">
              <label for="radio-origin-fr">French</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-de" value="de">
              <label for="radio-origin-de">German</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-ja" value="ja">
              <label for="radio-origin-ja">Japanese</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-es" value="es">
              <label for="radio-origin-es">Spanish</label>
            </div>
          </div>
          <div>
            <b>Translate into</b>
            <div class="radio-spacer">
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-en" value="en">
              <label for="radio-dest-en">English</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-fr" value="fr">
              <label for="radio-dest-fr">French</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-de" value="de">
              <label for="radio-dest-de">German</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-ja" value="ja" checked="checked">
              <label for="radio-dest-ja">Japanese</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-es" value="es">
              <label for="radio-dest-es">Spanish</label>
            </div>
          </div>
        </div>
        <div class="block form-group">
          <label for="translated-text"><b>Translation</b></label>
          <textarea class="width-100" id="translated-text" rows="10"></textarea>
        </div>
        <div class="block">
          <input type="checkbox" id="save-prefs">
          <label for="save-prefs">Use these languages by default</label>
        </div>
        <div class="block" id="button-bar">
          <button class="blue" id="run-translation">Translate</button>
          <button id="insert-text">Insert</button>
        </div>
      </form>
    </div>
    
    <div class="sidebar bottom">
      <img alt="Add-on logo" class="logo" src="https://www.gstatic.com/images/branding/product/1x/translate_48dp.png" width="27" height="27">
      <span class="gray branding-text">Translate sample by Google</span>
    </div>
    
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script>
      /**
       * On document load, assign click handlers to each button and try to load the
       * user's origin and destination language preferences if previously set.
       */
      $(function() {
        $('#run-translation').click(runTranslation);
        $('#insert-text').click(insertText);
        google.script.run.withSuccessHandler(loadPreferences)
                .withFailureHandler(showError).getPreferences();
      });
    
      /**
       * Callback function that populates the origin and destination selection
       * boxes with user preferences from the server.
       *
       * @param {Object} languagePrefs The saved origin and destination languages.
       */
      function loadPreferences(languagePrefs) {
        $('input:radio[name="origin"]')
                .filter('[value=' + languagePrefs.originLang + ']')
                .attr('checked', true);
        $('input:radio[name="dest"]')
                .filter('[value=' + languagePrefs.destLang + ']')
                .attr('checked', true);
      }
    
      /**
       * Runs a server-side function to translate the user-selected text and update
       * the sidebar UI with the resulting translation.
       */
      function runTranslation() {
        this.disabled = true;
        $('#error').remove();
        const origin = $('input[name=origin]:checked').val();
        const dest = $('input[name=dest]:checked').val();
        const savePrefs = $('#save-prefs').is(':checked');
        google.script.run
                .withSuccessHandler(
                        function(textAndTranslation, element) {
                          $('#translated-text').val(textAndTranslation.translation);
                          element.disabled = false;
                        })
                .withFailureHandler(
                        function(msg, element) {
                          showError(msg, $('#button-bar'));
                          element.disabled = false;
                        })
                .withUserObject(this)
                .getTextAndTranslation(origin, dest, savePrefs);
      }
    
      /**
       * Runs a server-side function to insert the translated text into the document
       * at the user's cursor or selection.
       */
      function insertText() {
        this.disabled = true;
        $('#error').remove();
        google.script.run
                .withSuccessHandler(
                        function(returnSuccess, element) {
                          element.disabled = false;
                        })
                .withFailureHandler(
                        function(msg, element) {
                          showError(msg, $('#button-bar'));
                          element.disabled = false;
                        })
                .withUserObject(this)
                .insertText($('#translated-text').val());
      }
    
      /**
       * Inserts a div that contains an error message after a given element.
       *
       * @param {string} msg The error message to display.
       * @param {DOMElement} element The element after which to display the error.
       */
      function showError(msg, element) {
        const div = $('<div id="error" class="error">' + msg + '</div>');
        $(element).after(div);
      }
    </script>
    </body>
    </html>

Run the script

  1. In your Docs document, reload the page.
  2. Click Extensions > Translate Docs > Start.
  3. When prompted, authorize the add-on. Upon authorization, the add-on will restart.
  4. Type some text into your document and select it.
  5. In the add-on, click Translate. To replace the text in the document, click Insert.

Next steps

Design a Mobile Site
View Site in Mobile | Classic
Share by: