Serve a form in multiple languages
Auto-translate one Northwind form into localised versions — same questions, different copies.
Published Aug 18, 2025
Northwind’s community survey works in English, but a chunk of the audience reads French and Spanish. Maintaining three copies by hand is the kind of busywork that breaks the moment someone tweaks a question — one language drifts out of sync, then another, and the data nobody can compare any more.
This script keeps a single master form as the source of truth and clones it
into translated versions on demand. It reads the title, every question, and the
choices on multiple-choice items, sends each string through LanguageApp, and
writes the result back to the copy. Run it again whenever the master changes
and you get a fresh translation rather than a drifted one.
What you’ll need
- A master Google Form with all the questions written in one language (English here) — this is the one you actually edit. Save its ID in the script.
- A list of target language codes (BCP-47 short codes work —
fr,es,de). - Access to Apps Script’s built-in
LanguageAppservice. No API key needed, but it is rate-limited so do not loop it over thousands of items per run. - A place to put the copies. They land in the script owner’s Drive root by default; move them into a shared folder afterwards.
The script
// The master form — the only one you edit by hand.
const SOURCE = '1abcSourceFormId';
// Source language for everything in the master form.
const SOURCE_LANG = 'en';
// The item types that hold choice lists we also want translated.
const CHOICE_TYPES = [
FormApp.ItemType.MULTIPLE_CHOICE,
FormApp.ItemType.CHECKBOX,
FormApp.ItemType.LIST,
];
/**
* Clones the master form and translates every visible string into the target
* language. Run once per language you want to support.
*
* @param {string} targetLang - BCP-47 code like 'fr', 'es', 'de'.
* @returns {string} The new form's edit URL.
*/
function cloneAndTranslate(targetLang) {
if (!targetLang || targetLang === SOURCE_LANG) {
throw new Error('Provide a target language different from ' + SOURCE_LANG);
}
const source = FormApp.openById(SOURCE);
// 1. Duplicate the form file in Drive. makeCopy keeps the question
// structure, scoring, and trigger config intact.
const copyFile = DriveApp.getFileById(SOURCE)
.makeCopy(`${source.getTitle()} — ${targetLang}`);
const copy = FormApp.openById(copyFile.getId());
// 2. Translate the title and the description sitting under it.
copy.setTitle(translate(source.getTitle(), targetLang));
if (source.getDescription()) {
copy.setDescription(translate(source.getDescription(), targetLang));
}
// 3. Walk every item. The clone has the same items in the same order as
// the source, so we can iterate copy.getItems() directly.
for (const item of copy.getItems()) {
const title = item.getTitle();
if (title) item.setTitle(translate(title, targetLang));
const help = item.getHelpText();
if (help) item.setHelpText(translate(help, targetLang));
// 4. Multiple-choice, checkbox, and list items have a choice array
// that needs translating too.
if (CHOICE_TYPES.includes(item.getType())) {
translateChoices(item, targetLang);
}
}
Logger.log(`Created ${copy.getEditUrl()}`);
return copy.getEditUrl();
}
/**
* Translate the choices on a multiple-choice, checkbox, or list item.
*/
function translateChoices(item, targetLang) {
let list;
switch (item.getType()) {
case FormApp.ItemType.MULTIPLE_CHOICE:
list = item.asMultipleChoiceItem();
break;
case FormApp.ItemType.CHECKBOX:
list = item.asCheckboxItem();
break;
case FormApp.ItemType.LIST:
list = item.asListItem();
break;
default:
return;
}
const translated = list.getChoices()
.map((c) => translate(c.getValue(), targetLang));
list.setChoiceValues(translated);
}
/**
* Thin wrapper around LanguageApp so blank strings are a no-op and we have
* one place to handle failures.
*/
function translate(text, targetLang) {
if (!text) return text;
try {
return LanguageApp.translate(text, SOURCE_LANG, targetLang);
} catch (err) {
Logger.log(`Translate failed for "${text}" — ${err.message}`);
return text;
}
}
How it works
cloneAndTranslatetakes a target language code and refuses to translate into the source language, which would just be an expensive copy.- It opens the master form and uses
DriveApp.makeCopyto duplicate the underlying file. The copy keeps every question, validation rule, and section break — only the visible text needs replacing. - It translates the form title and description first, then walks every item in order. The copy’s items mirror the source’s, so iterating once is enough.
- For each item it translates the question title and any help text. Both can be blank, so the helper short-circuits empty strings.
- Multiple-choice, checkbox, and list items have a choice array. The helper converts to the right typed item, reads the current choices, and writes back the translated values.
translatewrapsLanguageApp.translateso a failure on one string logs a warning and returns the original text instead of stopping the whole run.
Example run
With a master form titled “Northwind community survey” and questions like
“How did you hear about us?”, calling cloneAndTranslate('fr') produces a new
form titled “Sondage communautaire Northwind” with the question translated to
“Comment avez-vous entendu parler de nous ?” and multiple-choice answers
translated in place.
Run it for each language you need:
function buildAllTranslations() {
['fr', 'es', 'de'].forEach(cloneAndTranslate);
}
The execution log lists the edit URL for each new form so you can open and share them.
Run it
This is a once-per-update job, not a continuous one. Translate when the master changes; do not run it on a daily trigger.
- In the Apps Script editor, open
cloneAndTranslateand edit theSOURCEconstant to point at your master form. - Either call
cloneAndTranslate('fr')directly from the editor, or wrap a batch call likebuildAllTranslationsand run that. - Approve the Drive, Forms, and translation authorisation prompts the first time.
- Move the new forms into your shared localisation folder and publish them.
Watch out for
LanguageAppis machine translation. It is fine for “Comment vous appelez-vous?” but it will mangle idioms and brand names. Have a native speaker read each form once before you publish — and keep a glossary for terms that must not be translated (product names, legal phrasing).- Responses go to separate sheets. Each cloned form has its own response destination — set one up per language, then unify them downstream with a language column rather than sharing one destination across forms.
- Re-running creates a new copy each time. If you want to overwrite an
existing translation, look it up by title and call
clearItemsbefore copying, instead of leaving four “Sondage communautaire Northwind” files cluttering Drive. - Daily quotas apply.
LanguageAppis generous but not unlimited; if you translate a 100-question form into ten languages on a schedule, batch the work and back off when calls start throwing.
Related
Trigger an onboarding sequence on form submit
Kick off tasks when a new Northwind hire submits their starter form.
Updated Oct 17, 2025
Build a content-submission queue
Collect Northwind guest posts or ideas for review through a Form.
Updated Oct 9, 2025
Score sentiment in open-text feedback
Rate Northwind feedback comments without manual review — using the in-Sheet sentiment function.
Updated Oct 5, 2025
Build a peer-nomination and voting system
Collect and tally Northwind nominations for awards or initiatives — one ballot, anonymous.
Updated Oct 1, 2025
Roll a form over each cycle
Archive old responses and reset for the next Northwind cycle — quarterly OKR check-ins.
Updated Sep 27, 2025