Build a multi-language document generator
Produce the same Northwind Doc in English, German, and Spanish from a single source of strings.
Published Aug 10, 2025
Northwind ships the same client welcome pack to studios in three countries, and for a while each language was its own Doc. Edit the English version and someone had to remember to mirror the change into German and Spanish — which, of course, nobody did. Translations drifted, and a client in Berlin ended up with last quarter’s pricing.
This script keeps one source of truth: a Strings sheet with one row per phrase
and one column per language. A single Doc template holds the layout with
{{placeholder}} markers, and the script stamps out a localised copy for every
language column. Change a string once and the next run rebuilds all three Docs in
step.
What you’ll need
- A Google Doc template with
{{placeholder}}markers where text should go — for example{{title}},{{intro}},{{signoff}}. The markers are the same in every language; only the values change. - A
StringsGoogle Sheet. The first column holds the placeholder key (without the braces), and each remaining column is one language — header rowkey,English,German,Spanish. - A Drive folder to hold the generated Docs.
- The three IDs above, pasted into the config block at the top of the script.
The script
// Doc template containing the {{placeholder}} markers.
const TEMPLATE = '1abcMultilangTemplateId';
// Sheet with one row per string and one column per language.
const STRINGS = '1abcStringsSheetId';
// Drive folder where the localised Docs are saved.
const OUTPUT = '1abcLocalisedFolderId';
/**
* Builds one localised Doc per language column in the Strings sheet,
* replacing every {{placeholder}} in the template with the matching
* value for that language.
*
* @param {string} name - Base name for the generated Docs, e.g. "Welcome pack".
*/
function buildLocalisedDocs(name) {
// 1. Read the Strings sheet in one call: header row plus data rows.
const [header, ...rows] = SpreadsheetApp.openById(STRINGS).getSheets()[0]
.getDataRange().getValues();
// 2. The first column is the key; every other header is a language.
const languages = header.slice(1);
if (!languages.length || !rows.length) {
Logger.log('Strings sheet has no languages or no rows — nothing to do.');
return;
}
// 3. Build a lookup: key -> array of values, one per language column.
const strings = Object.fromEntries(rows.map((r) => [r[0], r.slice(1)]));
const folder = DriveApp.getFolderById(OUTPUT);
// 4. One pass per language column.
for (let i = 0; i < languages.length; i++) {
const lang = languages[i];
// 5. Copy the template into the output folder, named per language.
const copy = DriveApp.getFileById(TEMPLATE)
.makeCopy(`${name} — ${lang}`, folder);
// 6. Open the copy and swap every placeholder for this language's value.
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
for (const [key, values] of Object.entries(strings)) {
body.replaceText(`{{${key}}}`, values[i] || '');
}
doc.saveAndClose();
Logger.log(`Built ${lang} copy: ${copy.getUrl()}`);
}
}
How it works
buildLocalisedDocsopens theStringssheet and reads it in a single call, splitting the header row off from the data rows.- Everything after the first header cell is treated as a language. The first column is the placeholder key.
- If there are no language columns or no rows, it logs a message and stops before copying anything.
- It builds a lookup object mapping each key to its row of translations, so
strings['title']returns['Welcome', 'Willkommen', 'Bienvenido']. - For each language it copies the template into the output folder, naming the copy with the language so the three Docs are easy to tell apart.
- It opens the copy and runs
replaceTextfor every key, picking the value at the column index for that language. A missing translation falls back to an empty string rather than leaving a raw{{marker}}in the Doc.
Example run
A small Strings sheet:
| key | English | German | Spanish |
|---|---|---|---|
| title | Welcome to Northwind | Willkommen bei Northwind | Bienvenido a Northwind |
| intro | We’re glad to have you. | Schön, dass Sie da sind. | Nos alegra tenerle. |
| signoff | The Northwind team | Ihr Northwind-Team | El equipo de Northwind |
Calling buildLocalisedDocs('Welcome pack') produces three Docs in the output
folder:
Welcome pack — EnglishWelcome pack — GermanWelcome pack — Spanish
Each one is the template with its {{title}}, {{intro}} and {{signoff}}
markers filled in from the matching column.
Run it
This is an on-demand job — run it when the strings change or a new pack is due:
- In the Apps Script editor, select
buildLocalisedDocs. - Because it takes a
nameargument, call it from a small wrapper rather than the Run button — addfunction buildWelcomePack() { buildLocalisedDocs('Welcome pack'); }and run that. - Approve the authorisation prompt the first time.
- Open the output folder to check the three Docs.
Watch out for
replaceTexttakes a regular expression. If a placeholder key contains a character like.or+, it will be treated as regex syntax. Stick to plain letters, digits, and underscores in keys to stay safe.- A blank cell in a language column produces an empty string in the Doc, not the
English fallback. If you want fallback behaviour, change
values[i] || ''tovalues[i] || values[0]. - This swaps text only. It does not translate — every language column must be
filled in by a human or a translation step.
LanguageApp.translatecan seed a draft, but machine output still needs a review before it reaches a client. - Each run creates fresh copies; it never updates the previous ones. Old Docs pile up in the folder, so clear or archive them between runs.
replaceTextdoes not reach into headers, footers, or text inside tables unless you also call it on those elements. If your template uses them, extend the loop to coverbody.getParent()regions as needed.
Related
Generate personalized study guides from notes
Reformat raw notes into structured study guides — for Northwind's internal training programme.
Updated Feb 8, 2026
Build a contract-clause assembly system
Construct Northwind agreements from a library of approved clauses — drag-drop in code.
Updated Feb 1, 2026
Translate and resolve Doc comments
Localise reviewer feedback on a shared Doc so multilingual teams can collaborate.
Updated Jan 25, 2026
Auto-archive finalized Docs to dated folders
File completed Northwind Docs by month so the active folder stays focused on in-flight work.
Updated Jan 18, 2026
Build a fillable intake form inside a Doc
Create structured intake forms with placeholder fields readers can fill — for client briefs.
Updated Jan 11, 2026