/**
* build-i18n.js — renders one static index.html per locale from a single template.
*
* Why static generation rather than swapping strings at runtime: the whole app is
* client-rendered into
, so a crawler only ever sees . A locale
* that has no URL of its own has no way of being indexed. Each locale therefore gets
* a real file at a real path, with its own title, description, canonical and hreflang
* cluster, produced at build time.
*
* Runs before post-build.js, which re-stamps ?v= and sw.js. The ?v=BUILD_VERSION
* placeholders are filled here from meta.json so the generated files are already
* correct on their own; post-build's later pass over index.html is idempotent.
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const TEMPLATE = path.join(ROOT, 'templates', 'index.template.html');
// Overridable so the test can render a throwaway multi-locale site into a temp
// directory: the machinery that matters here only shows itself with two locales,
// and a half-translated locale is not something to ship just to exercise it.
const LOCALES_DIR = process.env.I18N_LOCALES_DIR || path.join(ROOT, 'locales');
const OUT_ROOT = process.env.I18N_OUT_ROOT || ROOT;
const read = (p) => fs.readFileSync(p, 'utf8');
const readJson = (p) => JSON.parse(read(p));
/** Escape the four characters that can break out of a double-quoted attribute. */
const attr = (value) =>
String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
/** Public URL of a locale. The default locale owns the root, so existing links keep working. */
function localeUrl(site, code) {
return code === site.defaultLocale ? `${site.baseUrl}/` : `${site.baseUrl}/${code}/`;
}
/** Where the rendered file lands on disk. */
function outputPath(site, code) {
return code === site.defaultLocale
? path.join(OUT_ROOT, 'index.html')
: path.join(OUT_ROOT, code, 'index.html');
}
/**
* hreflang cluster. Every locale lists every locale including itself — Google treats a
* cluster that omits its own page as invalid and ignores it. A single-locale site gets
* no cluster at all, since there is nothing to point at.
*/
function hreflangLinks(site) {
if (site.locales.length < 2) return '';
const links = site.locales.map(
(code) => ` `
);
links.push(` `);
return links.join('\n');
}
function ogLocaleAlternate(site, code) {
if (site.locales.length < 2) return '';
return site.locales
.filter((other) => other !== code)
.map((other) => ` `)
.join('\n');
}
/**
* schema.org graph, rebuilt per locale so the descriptions and feature list are
* translated too — a page whose visible text is German and whose structured data is
* English is describing something other than itself.
*/
function structuredData(site, code) {
const locale = site.byCode[code];
const url = localeUrl(site, code);
const graph = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebSite',
'@id': `${site.baseUrl}/#website`,
name: site.siteName,
url: `${site.baseUrl}/`,
description: locale.schema.siteDescription,
inLanguage: locale.htmlLang,
},
{
'@type': 'WebApplication',
name: site.siteName,
url,
applicationCategory: 'CommunicationApplication',
operatingSystem: locale.schema.operatingSystem,
browserRequirements: locale.schema.browserRequirements,
description: locale.schema.appDescription,
inLanguage: locale.htmlLang,
image: site.baseUrl + site.socialCard,
license: 'https://opensource.org/licenses/MIT',
isAccessibleForFree: true,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
featureList: locale.schema.featureList,
sameAs: [site.repository],
},
],
};
// Indent to sit under the