Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions slides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Sample Google Apps Script add-ons for Google Slides.

This add-on translates selected text from one language to another.

### Enhanced Version Available

An enhanced version is also available (`translateAllSlides.gs` and `sidebarEnhanced.html`) with additional features:
- Translate entire presentations with one click
- Source language selection with auto-detect option
- Expanded support for 30+ languages
- Two translation modes: selected elements or all slides

![Translate](https://user-images.githubusercontent.com/380123/45050204-9f383a00-b04e-11e8-9dc8-30fcc5e9fdd7.png)

## [Progress Bars](https://developers.google.com/apps-script/guides/slides/samples/progress-bar)
Expand Down
181 changes: 181 additions & 0 deletions slides/translate/sidebarEnhanced.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<!--
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
<style>
.logo { vertical-align: middle; }
ul { list-style-type: none; padding: 0; }
h4 { margin: 10px 0 5px 0; }
.error { color: #d32f2f; }
.success { color: #388e3c; }
#button-bar button { margin: 5px 0; width: 100%; }
</style>
</head>
<body>
<form class="sidebar branding-below">
<h4>Source language:</h4>
<ul id="source-languages"></ul>

<h4>Target language:</h4>
<ul id="target-languages"></ul>

<div class="block" id="button-bar">
<button class="blue" id="run-translation">Translate Selected</button>
<button class="blue" id="run-all-translation">Translate All Slides</button>
</div>
<h5 class="error" id="error"></h5>
<h5 class="success" id="success"></h5>
</form>
<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="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
$(function() {
// Expanded list of languages for translation
const languages = {
ar: 'Arabic',
bg: 'Bulgarian',
zh: 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
hr: 'Croatian',
cs: 'Czech',
da: 'Danish',
nl: 'Dutch',
en: 'English',
fi: 'Finnish',
fr: 'French',
de: 'German',
el: 'Greek',
he: 'Hebrew',
hi: 'Hindi',
hu: 'Hungarian',
id: 'Indonesian',
it: 'Italian',
ja: 'Japanese',
ko: 'Korean',
no: 'Norwegian',
pl: 'Polish',
pt: 'Portuguese',
ro: 'Romanian',
ru: 'Russian',
es: 'Spanish',
sv: 'Swedish',
th: 'Thai',
tr: 'Turkish',
uk: 'Ukrainian',
vi: 'Vietnamese'
};

// Create radio buttons for source languages
const sourceLanguageList = Object.keys(languages).map((id) => {
return $('<li>').html([
$('<input>')
.attr('type', 'radio')
.attr('name', 'source')
.attr('id', 'radio-source-' + id)
.attr('value', id)
.prop('checked', id === 'ja'), // Japanese selected by default
$('<label>')
.attr('for', 'radio-source-' + id)
.html(languages[id])
]);
});

// Create radio buttons for target languages
const targetLanguageList = Object.keys(languages).map((id) => {
return $('<li>').html([
$('<input>')
.attr('type', 'radio')
.attr('name', 'dest')
.attr('id', 'radio-dest-' + id)
.attr('value', id)
.prop('checked', id === 'en'), // English selected by default
$('<label>')
.attr('for', 'radio-dest-' + id)
.html(languages[id])
]);
});

$('#run-translation').click(runTranslation);
$('#run-all-translation').click(runAllTranslation);
$('#source-languages').html(sourceLanguageList);
$('#target-languages').html(targetLanguageList);
});

/**
* Runs a server-side function to translate the text on selected slides/elements.
*/
function runTranslation() {
this.disabled = true;
$('#error').text('');
$('#success').text('');
const sourceLanguage = $('input[name=source]:checked').val();
const targetLanguage = $('input[name=dest]:checked').val();
google.script.run
.withSuccessHandler((numTranslatedElements, element) =>{
element.disabled = false;
if (numTranslatedElements === 0) {
$('#error').empty()
.append('Did you select elements to translate?')
.append('<br/>')
.append('Please select slides or individual elements.');
} else {
$('#success').text('Successfully translated ' + numTranslatedElements + ' element(s)!');
}
return false;
})
.withFailureHandler((msg, element)=> {
element.disabled = false;
$('#error').text('Something went wrong. Please check the add-on logs.');
return false;
})
Comment on lines +142 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The failure handler for runTranslation currently shows a generic error message and ignores the specific error details (msg) returned from the server. This makes it difficult for users to understand what went wrong. To improve user experience, you should display the msg content in the error message, similar to how it's done in runAllTranslation's failure handler.

Suggested change
.withFailureHandler((msg, element)=> {
element.disabled = false;
$('#error').text('Something went wrong. Please check the add-on logs.');
return false;
})
.withFailureHandler((msg, element)=> {
element.disabled = false;
$('#error').text('Something went wrong: ' + msg);
return false;
})

.withUserObject(this)
.translateSelectedElements(sourceLanguage, targetLanguage);
}

/**
* Runs a server-side function to translate all text on all slides.
*/
function runAllTranslation() {
this.disabled = true;
$('#error').text('');
$('#success').text('');
const sourceLanguage = $('input[name=source]:checked').val();
const targetLanguage = $('input[name=dest]:checked').val();

google.script.run
.withSuccessHandler((numTranslatedElements, element) => {
element.disabled = false;
if (numTranslatedElements === 0) {
$('#error').text('No text elements found to translate.');
} else {
$('#success').text('Successfully translated ' + numTranslatedElements + ' element(s) across all slides!');
}
return false;
})
.withFailureHandler((msg, element) => {
element.disabled = false;
$('#error').text('Something went wrong: ' + msg);
return false;
})
.withUserObject(this)
.translateAllSlides(sourceLanguage, targetLanguage);
}
</script>
</body>
</html>
181 changes: 181 additions & 0 deletions slides/translate/translateAllSlides.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Copyright Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @OnlyCurrentDoc Limits the script to only accessing the current presentation.
*/

/**
* Create a open translate menu item.
* @param {object} event The open event.
*/
function onOpen(event) {
SlidesApp.getUi()
.createAddonMenu()
.addItem("Open Translate", "showSidebar")
.addToUi();
}

/**
* Open the Add-on upon install.
* @param {object} event The install event.
*/
function onInstall(event) {
onOpen(event);
}

/**
* Opens a sidebar in the document containing the add-on's user interface.
*/
function showSidebar() {
const ui =
HtmlService.createHtmlOutputFromFile("sidebarEnhanced").setTitle(
"Translate",
);
SlidesApp.getUi().showSidebar(ui);
}

/**
* Recursively gets child text elements a list of elements.
* @param {GoogleAppsScript.Slides.PageElement[]} elements The elements to get text from.
* @return {GoogleAppsScript.Slides.TextRange[]} An array of text elements.
*/
function getElementTexts(elements) {
/** @type {GoogleAppsScript.Slides.TextRange[]} */
let texts = [];
for (const element of elements) {
switch (element.getPageElementType()) {
case SlidesApp.PageElementType.GROUP:
for (const child of element.asGroup().getChildren()) {
texts = texts.concat(getElementTexts([child]));
}
break;
case SlidesApp.PageElementType.TABLE: {
const table = element.asTable();
for (let r = 0; r < table.getNumRows(); ++r) {
for (let c = 0; c < table.getNumColumns(); ++c) {
texts.push(table.getCell(r, c).getText());
}
}
break;
}
case SlidesApp.PageElementType.SHAPE: {
const shape = element.asShape();
// Only process shapes that have text
if (shape.getText) {
try {
const text = shape.getText();
if (text.asRenderedString().trim().length > 0) {
texts.push(text);
}
} catch (e) {
// Skip shapes that don't support text (images, etc.)
}
}
break;
}
}
}
return texts;
}

/**
* Translates selected slide elements to the target language using Apps Script's Language service.
*
* @param {string} sourceLanguage The two-letter short form for the source language. (ISO 639-1)
* @param {string} targetLanguage The two-letter short form for the target language. (ISO 639-1)
* @return {number} The number of elements translated.
*/
function translateSelectedElements(sourceLanguage, targetLanguage) {
// Get selected elements.
const selection = SlidesApp.getActivePresentation().getSelection();
const selectionType = selection.getSelectionType();
/** @type {GoogleAppsScript.Slides.TextRange[]} */
let texts = [];
switch (selectionType) {
case SlidesApp.SelectionType.PAGE:
for (const page of selection.getPageRange().getPages()) {
texts = texts.concat(getElementTexts(page.getPageElements()));
}
break;
case SlidesApp.SelectionType.PAGE_ELEMENT: {
const pageElements = selection.getPageElementRange().getPageElements();
texts = texts.concat(getElementTexts(pageElements));
break;
}
case SlidesApp.SelectionType.TABLE_CELL:
for (const cell of selection.getTableCellRange().getTableCells()) {
texts.push(cell.getText());
}
break;
case SlidesApp.SelectionType.TEXT:
for (const element of selection.getPageElementRange().getPageElements()) {
if (element.getPageElementType() === SlidesApp.PageElementType.SHAPE) {
texts.push(element.asShape().getText());
}
}
break;
}

// Translate all elements in-place.
for (const text of texts) {
text.setText(
LanguageApp.translate(
text.asRenderedString(),
sourceLanguage,
targetLanguage,
),
);
}

return texts.length;
Comment on lines +133 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function makes individual calls to LanguageApp.translate in a loop. This is inefficient and can lead to quota issues, especially when many elements are selected. It's much better to batch these calls. You can do this by collecting all text strings into an array, translating them with a single API call, and then updating the elements. This also allows you to return a more accurate count of translated elements.

  // Translate all elements in-place.
  const textsToTranslate = [];
  const originalStrings = [];
  for (const text of texts) {
    const originalText = text.asRenderedString();
    if (originalText.trim().length > 0) {
      textsToTranslate.push(text);
      originalStrings.push(originalText);
    }
  }

  if (originalStrings.length > 0) {
    const translatedStrings = LanguageApp.translate(
      originalStrings,
      sourceLanguage,
      targetLanguage,
    );
    for (let i = 0; i < textsToTranslate.length; i++) {
      textsToTranslate[i].setText(translatedStrings[i]);
    }
  }

  return textsToTranslate.length;

}

/**
* Translates all text elements in all slides of the active presentation.
*
* @param {string} sourceLanguage The two-letter short form for the source language. (ISO 639-1) Use empty string for auto-detect.
* @param {string} targetLanguage The two-letter short form for the target language. (ISO 639-1)
* @return {number} The number of elements translated.
*/
function translateAllSlides(sourceLanguage, targetLanguage) {
// Get the active presentation and all slides
const presentation = SlidesApp.getActivePresentation();
const slides = presentation.getSlides();

let totalTranslated = 0;

// Loop through each slide
for (const slide of slides) {
// Get all text-containing elements from the slide
const texts = getElementTexts(slide.getPageElements());

// Translate all text elements in-place
for (const text of texts) {
const originalText = text.asRenderedString();
if (originalText.trim().length > 0) {
const translatedText = LanguageApp.translate(
originalText,
sourceLanguage,
targetLanguage,
);
text.setText(translatedText);
totalTranslated++;
}
}
}

return totalTranslated;
}
Comment on lines +153 to +181
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The translateAllSlides function calls LanguageApp.translate for each text element individually. For presentations with many text elements, this is inefficient and can easily exceed Apps Script quotas for service calls. To improve performance and avoid quota issues, you should batch these calls. Collect all text strings into an array, make a single call to LanguageApp.translate, and then iterate through the results to update the text elements.

function translateAllSlides(sourceLanguage, targetLanguage) {
  // Get the active presentation and all slides
  const presentation = SlidesApp.getActivePresentation();
  const slides = presentation.getSlides();

  const textsToTranslate = [];
  const originalStrings = [];

  // Loop through each slide to gather all text elements
  for (const slide of slides) {
    const texts = getElementTexts(slide.getPageElements());
    for (const text of texts) {
      const originalText = text.asRenderedString();
      if (originalText.trim().length > 0) {
        textsToTranslate.push(text);
        originalStrings.push(originalText);
      }
    }
  }

  if (originalStrings.length === 0) {
    return 0;
  }

  // Translate all strings in a single batch call
  const translatedStrings = LanguageApp.translate(originalStrings, sourceLanguage, targetLanguage);

  // Update all text elements with translated content
  for (let i = 0; i < textsToTranslate.length; i++) {
    textsToTranslate[i].setText(translatedStrings[i]);
  }

  return textsToTranslate.length;
}