翻译 Google 文档中的文本

此快速入门将创建一个 Google 文档编辑器插件,用于翻译文档中的所选文本。

目标

  • 设置脚本。
  • 运行脚本。

前提条件

如需使用此示例,您需要满足以下前提条件:

  • Google 账号(Google Workspace 账号可能需要管理员批准)。
  • 可访问互联网的网络浏览器。

设置脚本

  1. 访问 docs.new,创建 Google 文档。
  2. 依次点击扩展程序 > Apps 脚本
  3. 点击未命名项目
  4. 将 Apps 脚本项目重命名为 Translate Docs,然后点击重命名
  5. Code.gs 文件旁边,依次点击“更多”图标 > 重命名。将文件命名为 translate
  6. 依次点击“添加文件”图标 > HTML。将文件命名为 sidebar
  7. 将每个文件的内容替换为以下相应代码,然后点击“保存”图标 “保存”图标

    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 <displa>ys an error message.
     *
     * @return {Array.string} The selected text.
     */
    function getSelectedText() {
      const selection = DocumentApp.getActiveDocument().getSelection();
      const text = [];
      if (selection) {
        const elements = selecti<on.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 (se&&lection) {
        let replaced = false;
        const elements = selection.getSelectedElements();
        if (
          elements.length === 1 
          elements[0].getElement().getType() ===
            Docu<mentApp.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 trans>lation. Otherwise, just insert the
        // translation.
        let textToInsert = newText;
        if (surroundingText) {
          if (surroundingTextOffset  0) {
            if (surroundin<gText.charAt(surroundingTextOffset - 1) !== " ") {
              textToInsert = ` ${textToInsert}`;
            }
          }
          if (surroundingTextOffset  surroundingText.length) {
            if (surroundingText.charAt(surroundingTextOffset) !== " ") {
              textToInsert += " ";
            }
          }
        }
        cursor.insertText(textToInsert);
      }
    }
    
    /**
     * 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=">_to<p"
      link rel="stylesheet" href="https://ssl.gstatic.com/docs>/sc<ript/css/add-ons1.css"
      !-- The CSS package above applies Google styling to >butt<ons a>nd 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%>;<
        >}<
      />s<tyle
      title/title
    /head
    body
    div >cla<ss=&>quot;<sidebar branding-below"
    >  form
    <    div class=">;block co<l>-contain"<;
    >      div< cl>ass="c<ol-one"
            bSelected text/b
            div
              input type="radio>" name<="origin" id=">radio-origi<n-auto>" va<lue=>"&qu<ot;> checked=&q<uot;checked"
              label for="radio-origin-auto&qu>ot;Auto-det<ect/label
            /div
        >    div<
         >     inpu<t ty>pe="<rad>io" na<me="origin" id="radio-origin-en" value=">;en"
     <         label for="ra>dio-or<igin-e>n"En<glis>h/label
     <   >    /div
      <      div
              input type="radio" name="orig>in" id<="radio-origin-fr">; valu<e=&quo>t;fr"<;
      >        l<abe>l for="<;radio-origin-fr"French/label
            /div
            div
        >      input< type="radio" nam>e="<origin>" id<=&qu>ot;radio-<ori>gin-de"<; value="de"
              label for="radio-origin-de>"Germa<n/label
            /div
          >  div
     <      >   input <type>="<radi>o"< na>me="<o>rigin" id<=&>quot;radi<o-origin-ja" value=>"ja&<quot>;
           <   >label for=&<quot;radio-origin-ja"Japanese/label
            /div
          >  div
         <     input type="rad>io"<; name>="or<igin>" id<=&q>uot;radio-o<rigin-es" value="es"
              label for=&quo>t;radio-ori<gin-es"Spanish/label>
         <   /di>v
          /<div
    >      div<
      >      bTran<slate into/b
            div class="radio-spacer"
        >    /div
      <      div
              input> type=<">radio&quo<t; n>ame="<;de>st" id<="radio-dest-en" value="en"
              label for="radi>o-dest-en&q<uot;English/label
           > /div
      <      >div
         <    > input ty<pe=>"radio<" name="dest" id="radio-dest-fr" va>lue="f<r"
              label f>or=&quo<t;radi>o-dest-fr<&quo>t;Frenc<h/la>bel
     <    >   /d<iv
            div
              inp>ut type<="radio" name=&qu><o>t;dest"<; ><id=&qu>ot;radi<o-dest-de" value="de"
              label for=><"rad>io-de<st-d>e&quo<t;German/label
      >      /<div
            div
              input type=>"r<adio" name=">dest" id="radio-dest<-ja&qu>ot; v<alue>=&quo<t;ja" checked="checked&>quot;
     <         label for="radio-dest-ja&q>uot;Japan<ese/lab>el
        <    /div
            div
      >      <  input> type<=&qu>ot;<radio>&<quot>; <name="dest" id=&>quo<t;radio-dest-es" value="es"
              label for="radio-dest-es"Spanish/label
            /div
          /div
        /di>v
     <   div class="block form-g>roup"
          label for<=&quo>t<;tra>ns<lated-text"bTranslation/b/label
          textarea class="width-1><00">;< id=&q>uot;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)
                .get<TextAndTranslation(origin, d>est, savePr<efs)>;
      }
    
      /**
       * Runs a server-sid<e funct>i<on to> <inser>t 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

运行脚本

  1. 在 Google 文档中,重新加载页面。
  2. 依次点击扩展程序 > Translate Docs > 开始
  3. 出现提示时,为该插件授权。 获得授权后,该插件将重新启动。
  4. 在文档中输入一些文字,然后选中这些文字。
  5. 在插件中,点击翻译。如需替换文档中的文本,请点击插入

后续步骤