Programmatic text formatting in Google Sheets with Google Apps Script
As part of a recent side project Iāve been exploring ways to apply rich text formatting to Google Sheets programmatically. Google Workspace (previously G Suite) comes with an extensive suite of APIs for interacting with its products called Google Apps Script, a Javascript environment that runs in the cloud, where you can use a number of APIs for products like the Docs, Sheets, and Gmail.
My goal is to apply text formatting to number of strings for our translation partner to then translate. These strings may include any combination of things like templating tags (like liquid), angular syntax, and/or regular olā HTML. Any of these are to be highlight in red, meaning ādonāt translateā.
If our string was <p>hello</p>
, weād only want the word āhelloā translated, to it would stay black while the opening and closing tags are red. A more complex, visual example:
<p>Please <a target="_top" href="%retry.url%">click here</a> to try your purchase again.</p>
We would want to look like:
<p>Please <a target=ā_topā href=ā%retry.url%ā>click here</a> to try your purchase again.</p>
Turns out you can do this with Apps Script! After gathering some test cases, I divided the project into two software components:
- Formatting - Using App Script to read a Google Sheetās content, and apply formatting to it.
- Parsing - Function that intakes a string and return only the bits we want to be translated.
In this post Iām only going to address the formatting component, as the āParsingā component turned into something far more complicated than I anticipated.
Breaking ground
To get started with Apps Script, create a new Google Sheet in your Google drive. From the top menu navigate to āToolsā => āScript Editorā. This will open a new Apps Script project for this document. This is a fully-grown IDE for any Apps scripts where we will be putting our code. To find out more about the IDE and App scripts checkout the Apps Script developers site. When you first run your script you will probably need to grant the script some OAuth permissions to āSee, edit, create, and delete your spreadsheets in Google Driveā.
Adding a trigger
First thing I wanted to do is to hook up a trigger inside my document to run some code from my Apps Script. I used the reserved onOpen()
function, which is build in trigger that runs when a user opens a spreadsheet, document, presentation, or form that the user has permission to edit.
Inside this trigger function I add a new dropdown menu titled āMy Menuā to the Google Sheet toolbar, with a single option that reads āRun Functionā and, when clicked, will run a function called myCustomFunction()
.
/**
* onOpen event from Google Sheets
*
* Adds our custom menu to the Sheet to allow us to run our functions at will.
*/
function onOpen() {
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Run Formatter', functionName: 'myCustomFunction'},
];
spreadsheet.addMenu('My Menu', menuItems);
}
Reading a range of values
Now we have a button that triggers a function, itās time to make it do things. For this example I only need the values from column A
, so Iām hard coding my specific input range to be A2:A999
. This represents column A, all the way down to the 999th row, plenty of results to get started with.
function myCustomFunction() {
// Grab a refernece to the current sheet (when you activate our menu button from `onOpen()`
var sheet = SpreadsheetApp.getActiveSpreadsheet();
// From that sheet get the "A2:A999" range
// @see https://developers.google.com/apps-script/reference/spreadsheet/range
var input = sheet.getRange("A2:A999");
// Returns a rectangular grid of values for this range.
// In our case a single array as we're only after one column.
var data = input.getValues();
// Filter out any values that are empty cells
var entries = data.filter((node) => node[0] !== '');
// Onwards...
}
Side note on debugging
A word of caution within using console.log
calls within App Scripts, these donāt log to the browser console in Google Apps Scripts, instead they write logs to the Google Cloud Platformās Stackdriver Logging service. Alternatively you can use the Logger class and then view the results in the IDE under View > Logs. A more direct approach is to use the Browser.msgBox()
to show a Google native message box within your application usage context.
Parsing values & using an external API
For the sake of reducing complexity of this example. Iām going to brush over how the REST endpoint logic works in detail but, in summary, it ingests an array of strings and returns an array of found results (a pair of start and end offsets of the string). We will use these start/end pairs as indexes to apply our custom formatting in a moment. The following is a reference of how to make a REST call inside Google App Scripts:
var options = {
'method' : 'post',
'payload' : {
'entries': JSON.stringify(entries) // ["<p>One</p><p>Two</p>"]
}
}
var translatableStrings = JSON.parse(UrlFetchApp.fetch('https://[...].ngrok.io/parse', options));
/* translatableStrings = [[[3, 5], [13, 15]]] */
translatableStrings.forEach((row, index) => {
// A reference to the value of our original string
const original = entries[index];
/* Time to do some formatting... */
});
Adding the UrlFetchApp()
method to your code will trigger an OAuth dialog to reappear as the permissions your app now needs have changed.
Rich text formatting
Weāve now got an array of our original values stored in entries
, and an array of offsets denoting the start and end of each substring that we want to be formatted in translatableStrings
. Time for formatting. I abstracted the formatting logic into its own function that accepts a string, an array of offsets (start & end pairs), and returns a rich formatted string.
/**
* Apply highlight formatting to input strings and other terrible function comments
*
* @param {string} input The string value we're formatting.
* @param {array} offsets An array of offset pairs.
* @returns {RichTextValue} A stylized text string used to represent cell text.
*/
function formatString(input, offsets) {
// Start by creating a container variable for a RichTextValue
// @see https://developers.google.com/apps-script/reference/spreadsheet/rich-text-value
var rich = SpreadsheetApp.newRichTextValue();
// Set its contents as the input string.
rich.setText(input);
// Create a textStyle rule
// @see https://developers.google.com/apps-script/reference/charts/text-style
var highlightStyle = SpreadsheetApp.newTextStyle();
// Change the forgraound color to red.
highlightStyle.setForegroundColor('#F00');
// Build the text style configruation for use.
var builtHighlightStyle = highlightStyle.build();
// Set our builtHighlightStyle styles to the range provided of our rich text string.
offsets.forEach((offset) => {
rich.setTextStyle(offset[0], offset[1], builtHighlightStyle);
});
// Finally we return the built full Rich text object
return rich.build();
}
Putting together the pieces
Building on the code snippet above where I was iterating over our REST APIās response. We now run each response through our formatting function. Each pair of numbers for the row will format the text, returning a formatted string block, which I then render back to my sheet, in the column besides the input.
translatableStrings.forEach((offsets, index) => {
// A reference to the value of our original string
const original = entries[index];
// Pass our original value and its associated offsets to our formatString function
const formattedOutput = formatString(original, offsets); // "<p>One</p><p>Two</p>", [[[3, 5], [13, 15]]]
// Grab a reference to our output cell adjacent to our input in column A.
const outputCell = sheet.getRange("B" + index);
// Set the value of our output to the rich formatting object that was returned from `formatString`
outputCell.setRichTextValue(format);
});
Summary
Apps Scripts are an awesome way to carry out some complex logic, and programmatically interact with Google services. Even thought this was a real simple, barely scratching the surface, project to start and get my hands dirty with it really did me give a good taste of the possibilities of what you can do with it.