快速入门:Google 文档的插件

完成本页其余步骤,并在大约五分钟内创建了用于翻译边栏中文字的 Google 文档插件。

设置

  1. 创建新的 Google 文档
  2. 在新文档中,选择菜单项 扩展程序> Apps 脚本。如果您看到欢迎屏幕,请点击空白项目
  3. 已将 Code.gs 重命名为 translate.gs
  4. 依次点击“添加文件” > HTML 以创建新的 HTML 文件。 将此文件命名为 sidebar(Apps 脚本会自动添加 .html 扩展名)。
  5. translate.gssidebar.html 的内容替换为以下代码。

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);
}

边栏

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>
  1. 点击“保存”图标
  2. 点击未命名项目,将脚本命名为“翻译快速入门”,然后点击重命名。系统会在多个位置(包括授权对话框)向最终用户显示脚本名称。

试试看

  1. 切换回文档并重新加载页面。
  2. 几秒钟后,插件快速入门菜单下将出现翻译快速入门子菜单。(如果您为脚本选择了其他名称,系统会改为显示该名称。)点击插件 > 翻译快速入门 > 开始
  3. 系统会显示一个对话框,告知您该脚本需要授权。 点击继续。然后,出现另一个对话框,请求对特定 Google 服务进行授权。仔细阅读该通知,然后点击允许
  4. 系统会显示一个边栏。如需对其进行测试,请在文档中输入一些文本,然后选择该文本并点击蓝色的翻译按钮。如需替换文档中的文本,请点击插入

发布

这是一个示例插件,我们的教程到此结束。如果您要开发真正的插件,最后一步是发布该插件,以供其他人查找和安装。

了解详情

如需继续了解如何使用 Apps 脚本扩展 Google 文档,请查看以下资源: