Skip to content

Commit 30d2d33

Browse files
feature(slides): add enhanced translation with source language selection
1 parent 748a7fb commit 30d2d33

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<!--
2+
Copyright 2018 Google LLC
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
https://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
-->
13+
<html>
14+
<head>
15+
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
16+
<style>
17+
.logo { vertical-align: middle; }
18+
ul { list-style-type: none; padding: 0; }
19+
h4 { margin: 10px 0 5px 0; }
20+
.error { color: #d32f2f; }
21+
.success { color: #388e3c; }
22+
#button-bar button { margin: 5px 0; width: 100%; }
23+
</style>
24+
</head>
25+
<body>
26+
<form class="sidebar branding-below">
27+
<h4>Source language:</h4>
28+
<ul id="source-languages"></ul>
29+
30+
<h4>Target language:</h4>
31+
<ul id="target-languages"></ul>
32+
33+
<div class="block" id="button-bar">
34+
<button class="blue" id="run-translation">Translate Selected</button>
35+
<button class="blue" id="run-all-translation">Translate All Slides</button>
36+
</div>
37+
<h5 class="error" id="error"></h5>
38+
<h5 class="success" id="success"></h5>
39+
</form>
40+
<div class="sidebar bottom">
41+
<img alt="Add-on logo" class="logo"
42+
src="https://www.gstatic.com/images/branding/product/1x/translate_48dp.png" width="27" height="27">
43+
<span class="gray branding-text">Translate sample by Google</span>
44+
</div>
45+
46+
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
47+
<script>
48+
$(function() {
49+
// Expanded list of languages with auto-detect option for source
50+
const languages = {
51+
'': 'Auto-detect',
52+
ar: 'Arabic',
53+
bg: 'Bulgarian',
54+
zh: 'Chinese (Simplified)',
55+
'zh-TW': 'Chinese (Traditional)',
56+
hr: 'Croatian',
57+
cs: 'Czech',
58+
da: 'Danish',
59+
nl: 'Dutch',
60+
en: 'English',
61+
fi: 'Finnish',
62+
fr: 'French',
63+
de: 'German',
64+
el: 'Greek',
65+
he: 'Hebrew',
66+
hi: 'Hindi',
67+
hu: 'Hungarian',
68+
id: 'Indonesian',
69+
it: 'Italian',
70+
ja: 'Japanese',
71+
ko: 'Korean',
72+
no: 'Norwegian',
73+
pl: 'Polish',
74+
pt: 'Portuguese',
75+
ro: 'Romanian',
76+
ru: 'Russian',
77+
es: 'Spanish',
78+
sv: 'Swedish',
79+
th: 'Thai',
80+
tr: 'Turkish',
81+
uk: 'Ukrainian',
82+
vi: 'Vietnamese'
83+
};
84+
85+
// Create radio buttons for source languages (including auto-detect)
86+
const sourceLanguageList = Object.keys(languages).map((id) => {
87+
return $('<li>').html([
88+
$('<input>')
89+
.attr('type', 'radio')
90+
.attr('name', 'source')
91+
.attr('id', 'radio-source-' + id)
92+
.attr('value', id)
93+
.prop('checked', id === ''), // Auto-detect selected by default
94+
$('<label>')
95+
.attr('for', 'radio-source-' + id)
96+
.html(languages[id])
97+
]);
98+
});
99+
100+
// Create radio buttons for target languages (excluding auto-detect)
101+
const targetLanguageList = Object.keys(languages).filter(id => id !== '').map((id) => {
102+
return $('<li>').html([
103+
$('<input>')
104+
.attr('type', 'radio')
105+
.attr('name', 'dest')
106+
.attr('id', 'radio-dest-' + id)
107+
.attr('value', id)
108+
.prop('checked', id === 'en'), // English selected by default
109+
$('<label>')
110+
.attr('for', 'radio-dest-' + id)
111+
.html(languages[id])
112+
]);
113+
});
114+
115+
$('#run-translation').click(runTranslation);
116+
$('#run-all-translation').click(runAllTranslation);
117+
$('#source-languages').html(sourceLanguageList);
118+
$('#target-languages').html(targetLanguageList);
119+
});
120+
121+
/**
122+
* Runs a server-side function to translate the text on selected slides/elements.
123+
*/
124+
function runTranslation() {
125+
this.disabled = true;
126+
$('#error').text('');
127+
$('#success').text('');
128+
google.script.run
129+
.withSuccessHandler((numTranslatedElements, element) =>{
130+
element.disabled = false;
131+
if (numTranslatedElements === 0) {
132+
$('#error').empty()
133+
.append('Did you select elements to translate?')
134+
.append('<br/>')
135+
.append('Please select slides or individual elements.');
136+
} else {
137+
$('#success').text('Successfully translated ' + numTranslatedElements + ' element(s)!');
138+
}
139+
return false;
140+
})
141+
.withFailureHandler((msg, element)=> {
142+
element.disabled = false;
143+
$('#error').text('Something went wrong. Please check the add-on logs.');
144+
return false;
145+
})
146+
.withUserObject(this)
147+
.translateSelectedElements($('input[name=dest]:checked').val());
148+
}
149+
150+
/**
151+
* Runs a server-side function to translate all text on all slides.
152+
*/
153+
function runAllTranslation() {
154+
this.disabled = true;
155+
$('#error').text('');
156+
$('#success').text('');
157+
const sourceLanguage = $('input[name=source]:checked').val();
158+
const targetLanguage = $('input[name=dest]:checked').val();
159+
160+
google.script.run
161+
.withSuccessHandler((numTranslatedElements, element) => {
162+
element.disabled = false;
163+
if (numTranslatedElements === 0) {
164+
$('#error').text('No text elements found to translate.');
165+
} else {
166+
$('#success').text('Successfully translated ' + numTranslatedElements + ' element(s) across all slides!');
167+
}
168+
return false;
169+
})
170+
.withFailureHandler((msg, element) => {
171+
element.disabled = false;
172+
$('#error').text('Something went wrong: ' + msg);
173+
return false;
174+
})
175+
.withUserObject(this)
176+
.translateAllSlides(sourceLanguage, targetLanguage);
177+
}
178+
</script>
179+
</body>
180+
</html>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Copyright Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/**
17+
* @OnlyCurrentDoc Limits the script to only accessing the current presentation.
18+
*/
19+
20+
/**
21+
* Create a open translate menu item.
22+
* @param {object} event The open event.
23+
*/
24+
function onOpen(event) {
25+
SlidesApp.getUi()
26+
.createAddonMenu()
27+
.addItem("Open Translate", "showSidebar")
28+
.addToUi();
29+
}
30+
31+
/**
32+
* Open the Add-on upon install.
33+
* @param {object} event The install event.
34+
*/
35+
function onInstall(event) {
36+
onOpen(event);
37+
}
38+
39+
/**
40+
* Opens a sidebar in the document containing the add-on's user interface.
41+
*/
42+
function showSidebar() {
43+
const ui =
44+
HtmlService.createHtmlOutputFromFile("sidebarEnhanced").setTitle(
45+
"Translate",
46+
);
47+
SlidesApp.getUi().showSidebar(ui);
48+
}
49+
50+
/**
51+
* Recursively gets child text elements a list of elements.
52+
* @param {GoogleAppsScript.Slides.PageElement[]} elements The elements to get text from.
53+
* @return {GoogleAppsScript.Slides.TextRange[]} An array of text elements.
54+
*/
55+
function getElementTexts(elements) {
56+
/** @type {GoogleAppsScript.Slides.TextRange[]} */
57+
let texts = [];
58+
for (const element of elements) {
59+
switch (element.getPageElementType()) {
60+
case SlidesApp.PageElementType.GROUP:
61+
for (const child of element.asGroup().getChildren()) {
62+
texts = texts.concat(getElementTexts([child]));
63+
}
64+
break;
65+
case SlidesApp.PageElementType.TABLE: {
66+
const table = element.asTable();
67+
for (let r = 0; r < table.getNumRows(); ++r) {
68+
for (let c = 0; c < table.getNumColumns(); ++c) {
69+
texts.push(table.getCell(r, c).getText());
70+
}
71+
}
72+
break;
73+
}
74+
case SlidesApp.PageElementType.SHAPE:
75+
texts.push(element.asShape().getText());
76+
break;
77+
}
78+
}
79+
return texts;
80+
}
81+
82+
/**
83+
* Translates selected slide elements to the target language using Apps Script's Language service.
84+
*
85+
* @param {string} targetLanguage The two-letter short form for the target language. (ISO 639-1)
86+
* @return {number} The number of elements translated.
87+
*/
88+
function translateSelectedElements(targetLanguage) {
89+
// Get selected elements.
90+
const selection = SlidesApp.getActivePresentation().getSelection();
91+
const selectionType = selection.getSelectionType();
92+
/** @type {GoogleAppsScript.Slides.TextRange[]} */
93+
let texts = [];
94+
switch (selectionType) {
95+
case SlidesApp.SelectionType.PAGE:
96+
for (const page of selection.getPageRange().getPages()) {
97+
texts = texts.concat(getElementTexts(page.getPageElements()));
98+
}
99+
break;
100+
case SlidesApp.SelectionType.PAGE_ELEMENT: {
101+
const pageElements = selection.getPageElementRange().getPageElements();
102+
texts = texts.concat(getElementTexts(pageElements));
103+
break;
104+
}
105+
case SlidesApp.SelectionType.TABLE_CELL:
106+
for (const cell of selection.getTableCellRange().getTableCells()) {
107+
texts.push(cell.getText());
108+
}
109+
break;
110+
case SlidesApp.SelectionType.TEXT:
111+
for (const element of selection.getPageElementRange().getPageElements()) {
112+
texts.push(element.asShape().getText());
113+
}
114+
break;
115+
}
116+
117+
// Translate all elements in-place.
118+
for (const text of texts) {
119+
text.setText(
120+
LanguageApp.translate(text.asRenderedString(), "", targetLanguage),
121+
);
122+
}
123+
124+
return texts.length;
125+
}
126+
127+
/**
128+
* Translates all text elements in all slides of the active presentation.
129+
*
130+
* @param {string} sourceLanguage The two-letter short form for the source language. (ISO 639-1) Use empty string for auto-detect.
131+
* @param {string} targetLanguage The two-letter short form for the target language. (ISO 639-1)
132+
* @return {number} The number of elements translated.
133+
*/
134+
function translateAllSlides(sourceLanguage, targetLanguage) {
135+
// Get the active presentation and all slides
136+
const presentation = SlidesApp.getActivePresentation();
137+
const slides = presentation.getSlides();
138+
139+
let totalTranslated = 0;
140+
141+
// Loop through each slide
142+
for (const slide of slides) {
143+
// Get all text-containing elements from the slide
144+
const texts = getElementTexts(slide.getPageElements());
145+
146+
// Translate all text elements in-place
147+
for (const text of texts) {
148+
const originalText = text.asRenderedString();
149+
if (originalText.trim().length > 0) {
150+
const translatedText = LanguageApp.translate(
151+
originalText,
152+
sourceLanguage,
153+
targetLanguage,
154+
);
155+
text.setText(translatedText);
156+
totalTranslated++;
157+
}
158+
}
159+
}
160+
161+
return totalTranslated;
162+
}

0 commit comments

Comments
 (0)