Translate Text Selection in a Google Doc using Google Apps Script

In this quick tutorial, we’re going to build a simple little menu add-on for Google Docs that lets you translate text into another language. Some of this functionality may be baked into Google Docs at this point, but this should be a good tutorial that wraps together a few different services and methods of augmenting Google Docs, including something new to me, the PropertiesService. 

 

via GIPHY

 

You can see the basic functionality of what we will build in the GIF above. But the basics of it are that we’ll add a few menu items to the Docs menu, then tie those menu items to functions that allow us to set the target language for our translation add-on and another function that will actually translate the target of our selection. 

So, let’s get started. 

Adding Menu Items

First, we’re going to add some menu items to our Google Doc menu. Each one of these items will tie into a function that we will write later to actually translate the selection and save our language selection into the PropertiesService object. 

function onOpen() {

var ui = DocumentApp.getUi(); 
  
ui.createMenu('Translation Tools')
.addItem('Translate Selection', 'translateSelection')
.addItem('Modify Language', 'modifyLanguage')
.addToUi(); 

}

We are going to write all of this code in the function onOpen, which is a function defined by Google Apps Script that will be automatically called when you open the Doc, Sheet, etc. that this script is bound to if it is a container-bound scripts.  

If you are looking for a more in-depth tutorial on how to create menus for Google Apps, I have a separate post on that that goes deep into the how-to’s of that process

 

Storing Language Preferences with PropertiesService 

One of the things I like about doing these tutorials is that I frequently use them as excuses to learn new things about Google Apps Script. The “new to me” thing about this particular article is the method of storing user data with the PropertiesService. The PropertiesService of Apps Script lets you save data in a key:value store, which is similar to a JSON object, that have a few different levels of scope

For this document, we’ll look at storing some preferences that are specific to a user using getUserProperties. 

function modifyLanguage() {

  var ui = DocumentApp.getUi(); 
  // Here we will use a ui.prompt instance to get some input from the user; 
  // while the prompt is open, the rest of the script will pause while we wait on user input 
  
  var response = ui.prompt('Choose the language used by the translation tools. This should be the two character language code, i.e. EN or en'); 
  
  if (response.getSelectedButton() == ui.Button.OK) {
    // If they click the OK button, we'll get the text they input into the prompt 
    var text = response.getResponseText(); 
    var lowercased = text.toLowerCase(); 
    PropertiesService.getUserProperties().setProperty('target_language', lowercased); 
  } else {
    Logger.log('The user clicked the close button in the dialog\'s title bar.');
  }


}

As usual, the code is fairly well commented, but I’ll give a little recap here of what is going on. First, we get an instance of the document UI, then fire off a prompt to have the user input the language code they want to set as the target language. 

As with other instances of Google Apps Script UI dialogs, this will pause the running script and wait for a response from the user. With this type of prompt, we write an if/else statement to process the possible response from the user. 

If they click OK, we get the response text, transform it to lower case, and then use the PropertiesService to set the user property ‘target_language’ to the language code the user has submitted. 

From here, we can write our main function to actually translate the text selection. 

Translating Text Selections

In terms of complicatedness, this function contains some of the most hair pulling functionality I’ve written in Google Apps Script in awhile. It took me a fair amount of experimenting to get what I wanted, and maybe even hacked a little bit to get there : )

 

function translateSelection () {

  var ui = DocumentApp.getUi(); 
  var selection = DocumentApp.getActiveDocument().getSelection();
  var rangeElements = selection.getRangeElements(); 
  
  // This part is a little bit complicated. Since the selection in a Google Doc returns a range, not text, 
  // we have to do some massaging to get the text. Since rangeElements is an array data type, we'll map it 
  // to a new array that returns the text. Then, once we have an array with text, we'll join that into a sentence
  // using array methods
  var text = rangeElements.map( function(element) {
    // Here we have to walk down the classes to get to usable text:
    // We go from rangeElement to Element to Text and then get the text as a string
    return element.getElement().asText().getText(); 
  })
  var string = text.join('');
  // Here we'll get a user property called target_language
  // PropertiesService is basically a key:value store like localStorage  
  var userDefinedLanguage = PropertiesService.getUserProperties().getProperty('target_language')
  // Next we do some checking and if userDefinedLanguage is undefined, we'll set a default
  var targetLanguage = userDefinedLanguage ? userDefinedLanguage : 'es';  
  var translation = LanguageApp.translate(string, 'en', targetLanguage);
  // Confirm the translation to be made; the script will pause execution here to wait for user input
  ui.alert('The following translation will be made: \n' + string + ' -> ' + translation);
  
  // Here we'll actually replace the document contents 
  DocumentApp.getActiveDocument().getBody().replaceText(string, translation) 

}

Getting Actual Selection Text 

Overall, the most difficult thing here was getting the text of the selection, meaning whatever is highlighted by the mouse when the function is triggered. That code is a bit tricky because of all of the different Element classes Google Apps use. The getSelection method returns a range, which are essentially X,Y coordinates of where the selection is in the document. From there, we get the RangeElements which are still not entirely useful to us, but I consider them an array of elements that hold the Elements. 

So, we take our array of RangeElements and use the .map method to transform it into a new array. The .map method works similar to the .forEach method, but produces a new array. So for each RangeElement in the array we call the getElement method on that, then asText() and finally getText() to get the text of that element, which could be a character, whitespace, punctuation, etc. 

After all of that, we have a variable text that should look something like this: 

text = ["t","h","i","s"," ","i","s"," ","t","e","x","t"];

It is basically our selection broken up into individual characters and spaces. To reassemble this, we can just use the .join array method to make one string. 

Translating Our String

Now that we have the string of our selection, we can retrieve the target language from the PropertiesService using the getProperty method. In this case, there is always the potential that no target language has been explicitly set, so we set a default if not: 

var userDefinedLanguage = PropertiesService.getUserProperties().getProperty('target_language')
var targetLanguage = userDefinedLanguage ? userDefinedLanguage : 'es';

I do that checking using the ternary operator, which is basically syntactical shortcut for if/else. The first argument, in this case userDefinedLangauge is evaluated for true/false. If true, the variable targetLangauge is set to userDefineLangauge, but if false, we set it to ‘es.’ I’m relying on what the PropertyService returns here if nothing has been set, which is undefined. 

Once we have the target language as a variable, we can use the LanguageApp to actually do the translating: 

var translation = LanguageApp.translate(string, 'en', targetLanguage);

In this example, I am hardcoding the source language as ‘en,’ but in a more comprehensive example, we may want to make that configurable as well. The translate method of the LanguageApp takes the string we want to translate, the source language, and the target language, returning the translated string. 

In the more comprehensive code snippet above, I actually show the translation in a UI for the user to confirm, but won’t go over that in depth. From there, we just replace the original selection with the translated text: 

DocumentApp.getActiveDocument().getBody().replaceText(string, translation)

Wrapping Up 

Overall, I learned a lot of new and useful things from writing this post. First, this was my first experience with the LanguageApp, and it’s very cool to learn about the functionality available here if you do anything regarding translation. Second, this was also my first rodeo with the PropertiesService available through Apps Script, so I’m really intrigued as to possible uses for this in storing user preferences that persist across script sessions. 

Related Posts

Here are just a few posts that might be of interest to you or dig a bit deeper into things I’ve only glossed over here relating to Google Apps Script: 

 

 

 

 

7 thoughts on “Translate Text Selection in a Google Doc using Google Apps Script”

  1. Jimmy says:

    Hey Jeff,

    This is really cool…

    Do you have some guidance on simplifying this slightly? I just want a user to be able to translate the entire body of the document into a different language (with a drop-down list of specified languages from a TRANSLATE THIS DOCUMENT menu item)

    Is this possible?

    Feels simplier but not sure it is? It will allow my users to translate policy documents into a different language in VIEW mode rather than needing to create a copy etc.

    Almost want a getBody() of active document and then translate that?

    1. BrownBearWhatDoYouSee says:

      Hey Jimmy,

      I’m glad you found this an interesting tool. I’m guessing by your comment, you’ve already looked at native Google Docs functionality to do translation.

      I tried to play around with the the concept you mention, and it wasn’t as straightforward as I expected. I see two reasons for that: first, this line of code that does the translating var translation = LanguageApp.translate(string, 'en', targetLanguage); needs us to pass the source language code as well, which is hardcoded as English here. What you propose would need either source language detection and a map to language codes (‘es’,’fr’, etc.) or a data layer to store the current language code somewhere. Second, my initial example with the selection was pretty simple, so I tried to just do a verbatim translation of the document body with headings, and that seemed to complicate things somehow.

      Thanks for reaching out and I’m interested to see if you can get something working that does what you need. Comment back here if you want to bounce some ideas around : )

      Cheers, JE

  2. This tutorial was absolutely brilliant. I am using what you did here to synthesize speech in my document to read back my text selection. I have a problem though. Every time I select some text, it reads me the entire line, instead of my selected text. I’m running some tests to see if there is something I can do to alleviate this problem. Still, this is the CLOSEST thing ever to get where I am going. What you’ve done is nothing short of impressive. I’m looking forward to your future GAS work.

    1. BrownBearWhatDoYouSee says:

      Wow, Nikki! Thanks for the kind words. I’m glad this was so helpful to your own work, so be sure to share a link back here if you want an audience for it. That sounds like a really cool feature that could have some powerful impact for accessibility. My only recommendation would be to use the alert dialog to maybe print out the selection as a way to start debugging. Other than that, I can’t think of any way to observe the text selection while debugging.

      Best of luck and thanks for reading/subscribing!

      Regards,
      JE

  3. Thanks again! I have set up alerts to check the text before reading it out loud. I have utilized the ability to inject javascript into the an html file in order to trick the sheet into using the Speech Synthesis API. I’m sure there is a cleaner way to so what I’ve done so far. Here is a link to the sheet:

    https://docs.google.com/document/d/1qI8GCmRbRKcPd6FFCIKOtkBZf21Pf-pWXSOC4sTX9e0/edit?usp=sharing

    The Custom menu in the UI is where I have used my experimental functions.

  4. LUCY AMBER says:

    Very helpful post ..!

    1. BrownBearWhatDoYouSee says:

      Thanks for reading and the comment!

Leave a Reply

Your email address will not be published. Required fields are marked *