-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(slides): add enhanced translation with source language selection and translate-all functionality #603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
30d2d33
408c36e
fb93dbc
5ca3af5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| }) | ||
| .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> | ||
| 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()); | ||
| } | ||
happygoluckycodeeditor marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function makes individual calls to // 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 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;
} |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The failure handler for
runTranslationcurrently 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 themsgcontent in the error message, similar to how it's done inrunAllTranslation's failure handler.