appscript.dev
Automation Advanced Forms

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 LanguageApp service. 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

  1. cloneAndTranslate takes a target language code and refuses to translate into the source language, which would just be an expensive copy.
  2. It opens the master form and uses DriveApp.makeCopy to duplicate the underlying file. The copy keeps every question, validation rule, and section break — only the visible text needs replacing.
  3. 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.
  4. For each item it translates the question title and any help text. Both can be blank, so the helper short-circuits empty strings.
  5. 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.
  6. translate wraps LanguageApp.translate so 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.

  1. In the Apps Script editor, open cloneAndTranslate and edit the SOURCE constant to point at your master form.
  2. Either call cloneAndTranslate('fr') directly from the editor, or wrap a batch call like buildAllTranslations and run that.
  3. Approve the Drive, Forms, and translation authorisation prompts the first time.
  4. Move the new forms into your shared localisation folder and publish them.

Watch out for

  • LanguageApp is 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 clearItems before copying, instead of leaving four “Sondage communautaire Northwind” files cluttering Drive.
  • Daily quotas apply. LanguageApp is 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