Skip to content

Instantly share code, notes, and snippets.

@vcarl
Last active October 3, 2020 03:46
Show Gist options
  • Save vcarl/d13548af8c8a6431e89a9436c1706d28 to your computer and use it in GitHub Desktop.
Save vcarl/d13548af8c8a6431e89a9436c1706d28 to your computer and use it in GitHub Desktop.
Gatsby i18n page generation

This also assumes constants of

exports.defaultLocale = "en";
exports.supportedLanguages = ["en"];

translatedLocales coming from createI18nPages.js is important, and needs behavior for how to display missing translations in the frontend as well. It's how we know what other pages exist so we can put them in the sitemap as alternates (which was kind of a pain, had to totally take over output from the plugin) as well as listing them in <head> as <link rel="alternate"> tags

createI18nPages is where the real page generation happens. The gist is, make a primary page, then make /:locale/:url pages for all available translations. each page needs an array of translations, and reqs were to show the default translation if none was available

// …
const { groupBy } = require("lodash");
const { defaultLocale, supportedLanguages } = require("./i18n");
const { buildRoute } = require("./routes");
const createCustomPage = ({ actions, page, translatedLocales, route }) => {
const { createPage } = actions;
const { component, locale, context } = page;
const path = buildRoute(locale, route);
createPage({
path,
component,
context: {
...context,
locale,
urlPath: path,
// List alternate pages so we can include head <link>s to them
alternateUrls: translatedLocales
.filter((l) => l !== locale)
.map((l) => ({
locale: l,
path: buildRoute(l, route),
})),
},
});
};
exports.createI18nPages = ({ actions, pages }) => {
Object.entries(groupBy(pages, "route")).forEach(([route, pages]) => {
// We need to know which languages are translated so we can provide
// alternates URLs for SEO.
const translatedLocales = supportedLanguages.filter((locale) =>
pages.some((page) => page.locale === locale),
);
// For each localized page loaded, tell Gatsby about it.
pages.forEach((pageTranslation) => {
createCustomPage({
actions,
page: pageTranslation,
translatedLocales,
route,
});
});
// If there are any locales that don't have translated pages available,
// create a page with the default locale's version.
const missingLocales = supportedLanguages.filter(
(locale) => !pages.some((page) => page.locale === locale),
);
const defaultPage = pages.find((page) => page.locale === defaultLocale);
missingLocales.forEach((missingLocale) => {
createPage({
actions,
page: {
...defaultPage,
path: buildRoute(missingLocale, route),
locale: missingLocale,
context: {
...defaultPage.context,
translated: false,
},
},
translatedLocales,
route,
});
});
});
};
// …
// …
exports.createMdxPages = ({ actions, files, component, baseRoute, type }) => {
const pages = files.edges.map(({ node }) => ({
...node,
route: `${baseRoute}/${node.relativeDirectory}`,
component,
locale: node.childMdx.fields.locale,
context: {
id: node.childMdx.id,
metadata: node.childMdx.frontmatter,
type,
},
}));
createI18nPages({ actions, pages });
};
// …
// …
const getLocale = (filename) => {
const nameSections = filename.split(".");
const length = nameSections.length;
// If there are 2+ sections after splitting on `.` (excluding file extension)
// and the last one is in our list of supported languages, that's our locale
if (length >= 2 && supportedLanguages.includes(nameSections[length - 1])) {
return nameSections[length - 1];
}
return defaultLocale;
};
exports.onCreateNode = ({ node, actions }) => {
const { createNodeField } = actions;
if (node.internal.type === "Mdx" && node.fileAbsolutePath) {
const locale = getLocale(path.parse(node.fileAbsolutePath).base);
createNodeField({
node,
name: "locale",
value: locale,
});
}
};
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions;
// All our custom page generation adds a `locale` key to context, but components
// sourced from Gatsby `pages/` don't go through any of those mechanisms. Add
// international routes for those.
// Possible future TODO: remove pages/, load those components with i18n helper.
if (!page.context.locale) {
deletePage(page);
createPage({
...page,
context: {
locale: defaultLocale,
urlPath: page.path,
contentfulLocale: contentfulLocale[defaultLocale],
lastModified: new Date().toISOString(),
// List alternate pages so we can include head <link>s to them
alternateUrls: supportedLanguages
.filter((l) => l !== defaultLocale)
.map((l) => ({
locale: l,
path: `/${l}${page.path}`,
})),
},
});
supportedLanguages.forEach((locale) => {
createPage({
...page,
path: `/${locale}${page.path}`,
context: {
...page.context,
locale,
urlPath: page.path,
contentfulLocale: contentfulLocale[locale],
lastModified: new Date().toISOString(),
// List alternate pages so we can include head <link>s to them
alternateUrls: supportedLanguages
.filter((l) => l !== locale)
.map((l) => ({
locale: l,
path: l === defaultLocale ? page.path : `/${l}${page.path}`,
})),
},
});
});
}
};
// …
const { groupBy } = require("lodash");
const { defaultLocale, supportedLanguages } = require("./i18n");
const getPathInfo = ({ path, context }) => {
const [_, maybeLocale, ...rest] = path.split("/");
const pathWithoutLocale = rest.join("/").replace(/\/$/, "");
// All of our translated pages begin with the locale code, like `/es/blah`.
// If the second section (because the leading slash means the 0th will be '')
// is in our list of supported languages, bam that's the locale. If not,
// that's the default locale, and it's a top level route. We only want to
// Keep top level routes for the default locale, so we need to know that.
if (supportedLanguages.includes(maybeLocale)) {
return {
locale: maybeLocale,
path: pathWithoutLocale,
originalPath: path,
isTopLevel: false,
context,
};
}
return {
locale: defaultLocale,
path: `${maybeLocale}/${pathWithoutLocale}`.replace(/\/$/, ""),
originalPath: path,
isTopLevel: true,
context,
};
};
const checkIsBlogPostRegex = /blog\/.+/;
const serializeLocale = (locale) => {
return ({ site, allSitePage }) => {
const { edges } = allSitePage;
const pages = edges.map(({ node }) => getPathInfo(node));
const byPath = groupBy(pages, "path");
const pagesWithAlternates = Object.values(byPath).reduce(
(accum, pageVersions) => {
const mainPage = pageVersions.find(
(pageVersion) => pageVersion.locale === locale,
);
const alternates = pageVersions.filter(
(pageVersion) => pageVersion.locale !== locale,
);
// Don't include blog posts on international sitemaps
if (
locale !== defaultLocale &&
checkIsBlogPostRegex.test(mainPage.originalPath)
) {
return accum;
}
accum.push({
main: mainPage,
alternates,
});
return accum;
},
[],
);
return pagesWithAlternates.map(({ main, alternates }) => {
return {
url: site.siteMetadata.siteUrl + main.originalPath,
lastmodISO: main.context.lastModified,
links: alternates.map((a) => ({
lang: a.locale,
url: site.siteMetadata.siteUrl + a.originalPath,
lastmodISO: a.context.lastModified,
})),
};
});
};
};
const query = `
{
site {
siteMetadata {
siteUrl
}
}
allSitePage {
edges {
node {
path
context {
lastModified
}
}
}
}
}`;
module.exports = {
serializeLocale,
query,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment