/** * 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