summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
committerShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
commit7a52ddeba2a68388b544f529d2d92104420f77b0 (patch)
tree15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js')
-rw-r--r--node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js317
1 files changed, 317 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js b/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js
new file mode 100644
index 0000000..6f53825
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/Plugins/I18nPlugin.js
@@ -0,0 +1,317 @@
+import { bcp47Normalize } from "bcp-47-normalize";
+import iso639 from "iso-639-1";
+import { DeepCopy } from "@11ty/eleventy-utils";
+
+// pathPrefix note:
+// When using `locale_url` filter with the `url` filter, `locale_url` must run first like
+// `| locale_url | url`. If you run `| url | locale_url` it won’t match correctly.
+
+// TODO improvement would be to throw an error if `locale_url` finds a url with the
+// path prefix at the beginning? Would need a better way to know `url` has transformed a string
+// rather than just raw comparison.
+// e.g. --pathprefix=/en/ should return `/en/en/` for `/en/index.liquid`
+
+class LangUtils {
+ static getLanguageCodeFromInputPath(filepath) {
+ return (filepath || "").split("/").find((entry) => Comparator.isLangCode(entry));
+ }
+
+ static getLanguageCodeFromUrl(url) {
+ let s = (url || "").split("/");
+ return s.length > 0 && Comparator.isLangCode(s[1]) ? s[1] : "";
+ }
+
+ static swapLanguageCodeNoCheck(str, langCode) {
+ let found = false;
+ return str
+ .split("/")
+ .map((entry) => {
+ // only match the first one
+ if (!found && Comparator.isLangCode(entry)) {
+ found = true;
+ return langCode;
+ }
+ return entry;
+ })
+ .join("/");
+ }
+
+ static swapLanguageCode(str, langCode) {
+ if (!Comparator.isLangCode(langCode)) {
+ return str;
+ }
+
+ return LangUtils.swapLanguageCodeNoCheck(str, langCode);
+ }
+}
+
+class Comparator {
+ // https://en.wikipedia.org/wiki/IETF_language_tag#Relation_to_other_standards
+ // Requires a ISO-639-1 language code at the start (2 characters before the first -)
+ static isLangCode(code) {
+ let [s] = (code || "").split("-");
+ if (!iso639.validate(s)) {
+ return false;
+ }
+ if (!bcp47Normalize(code)) {
+ return false;
+ }
+ return true;
+ }
+
+ static urlHasLangCode(url, code) {
+ if (!Comparator.isLangCode(code)) {
+ return false;
+ }
+
+ return url.split("/").some((entry) => entry === code);
+ }
+}
+
+function normalizeInputPath(inputPath, extensionMap) {
+ if (extensionMap) {
+ return extensionMap.removeTemplateExtension(inputPath);
+ }
+ return inputPath;
+}
+
+/*
+ * Input: {
+ * '/en-us/test/': './test/stubs-i18n/en-us/test.11ty.js',
+ * '/en/test/': './test/stubs-i18n/en/test.liquid',
+ * '/es/test/': './test/stubs-i18n/es/test.njk',
+ * '/non-lang-file/': './test/stubs-i18n/non-lang-file.njk'
+ * }
+ *
+ * Output: {
+ * '/en-us/test/': [ { url: '/en/test/' }, { url: '/es/test/' } ],
+ * '/en/test/': [ { url: '/en-us/test/' }, { url: '/es/test/' } ],
+ * '/es/test/': [ { url: '/en-us/test/' }, { url: '/en/test/' } ]
+ * }
+ */
+function getLocaleUrlsMap(urlToInputPath, extensionMap, options = {}) {
+ let filemap = {};
+
+ for (let url in urlToInputPath) {
+ // Group number comes from Pagination.js
+ let { inputPath: originalFilepath, groupNumber } = urlToInputPath[url];
+ let filepath = normalizeInputPath(originalFilepath, extensionMap);
+ let replaced =
+ LangUtils.swapLanguageCodeNoCheck(filepath, "__11ty_i18n") + `_group:${groupNumber}`;
+
+ if (!filemap[replaced]) {
+ filemap[replaced] = [];
+ }
+
+ let langCode = LangUtils.getLanguageCodeFromInputPath(originalFilepath);
+ if (!langCode) {
+ langCode = LangUtils.getLanguageCodeFromUrl(url);
+ }
+ if (!langCode) {
+ langCode = options.defaultLanguage;
+ }
+
+ if (langCode) {
+ filemap[replaced].push({
+ url,
+ lang: langCode,
+ label: iso639.getNativeName(langCode.split("-")[0]),
+ });
+ } else {
+ filemap[replaced].push({ url });
+ }
+ }
+
+ // Default sorted by lang code
+ for (let key in filemap) {
+ filemap[key].sort(function (a, b) {
+ if (a.lang < b.lang) {
+ return -1;
+ }
+ if (a.lang > b.lang) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+
+ // map of input paths => array of localized urls
+ let urlMap = {};
+ for (let filepath in filemap) {
+ for (let entry of filemap[filepath]) {
+ let url = entry.url;
+ if (!urlMap[url]) {
+ urlMap[url] = filemap[filepath].filter((entry) => {
+ if (entry.lang) {
+ return true;
+ }
+ return entry.url !== url;
+ });
+ }
+ }
+ }
+
+ return urlMap;
+}
+
+function eleventyI18nPlugin(eleventyConfig, opts = {}) {
+ let options = DeepCopy(
+ {
+ defaultLanguage: "",
+ filters: {
+ url: "locale_url",
+ links: "locale_links",
+ },
+ errorMode: "strict", // allow-fallback, never
+ },
+ opts,
+ );
+
+ if (!options.defaultLanguage) {
+ throw new Error(
+ "You must specify a `defaultLanguage` in Eleventy’s Internationalization (I18N) plugin.",
+ );
+ }
+
+ let extensionMap;
+ eleventyConfig.on("eleventy.extensionmap", (map) => {
+ extensionMap = map;
+ });
+
+ let bench = eleventyConfig.benchmarkManager.get("Aggregate");
+ let contentMaps = {};
+ eleventyConfig.on("eleventy.contentMap", function ({ urlToInputPath, inputPathToUrl }) {
+ let b = bench.get("(i18n Plugin) Setting up content map.");
+ b.before();
+ contentMaps.inputPathToUrl = inputPathToUrl;
+ contentMaps.urlToInputPath = urlToInputPath;
+
+ contentMaps.localeUrlsMap = getLocaleUrlsMap(urlToInputPath, extensionMap, options);
+ b.after();
+ });
+
+ eleventyConfig.addGlobalData("eleventyComputed.page.lang", () => {
+ // if addGlobalData receives a function it will execute it immediately,
+ // so we return a nested function for computed data
+ return (data) => {
+ return LangUtils.getLanguageCodeFromUrl(data.page.url) || options.defaultLanguage;
+ };
+ });
+
+ // Normalize a theoretical URL based on the current page’s language
+ // If a non-localized file exists, returns the URL without a language assigned
+ // Fails if no file exists (localized and not localized)
+ eleventyConfig.addFilter(options.filters.url, function (url, langCodeOverride) {
+ let langCode =
+ langCodeOverride ||
+ LangUtils.getLanguageCodeFromUrl(this.page?.url) ||
+ options.defaultLanguage;
+
+ // Already has a language code on it and has a relevant url with the target language code
+ if (
+ contentMaps.localeUrlsMap[url] ||
+ (!url.endsWith("/") && contentMaps.localeUrlsMap[`${url}/`])
+ ) {
+ for (let existingUrlObj of contentMaps.localeUrlsMap[url] ||
+ contentMaps.localeUrlsMap[`${url}/`]) {
+ if (Comparator.urlHasLangCode(existingUrlObj.url, langCode)) {
+ return existingUrlObj.url;
+ }
+ }
+ }
+
+ // Needs the language code prepended to the URL
+ let prependedLangCodeUrl = `/${langCode}${url}`;
+ if (
+ contentMaps.localeUrlsMap[prependedLangCodeUrl] ||
+ (!prependedLangCodeUrl.endsWith("/") && contentMaps.localeUrlsMap[`${prependedLangCodeUrl}/`])
+ ) {
+ return prependedLangCodeUrl;
+ }
+
+ if (
+ contentMaps.urlToInputPath[url] ||
+ (!url.endsWith("/") && contentMaps.urlToInputPath[`${url}/`])
+ ) {
+ // this is not a localized file (independent of a language code)
+ if (options.errorMode === "strict") {
+ throw new Error(
+ `Localized file for URL ${prependedLangCodeUrl} was not found in your project. A non-localized version does exist—are you sure you meant to use the \`${options.filters.url}\` filter for this? You can bypass this error using the \`errorMode\` option in the I18N plugin (current value: "${options.errorMode}").`,
+ );
+ }
+ } else if (options.errorMode === "allow-fallback") {
+ // You’re linking to a localized file that doesn’t exist!
+ throw new Error(
+ `Localized file for URL ${prependedLangCodeUrl} was not found in your project! You will need to add it if you want to link to it using the \`${options.filters.url}\` filter. You can bypass this error using the \`errorMode\` option in the I18N plugin (current value: "${options.errorMode}").`,
+ );
+ }
+
+ return url;
+ });
+
+ // Refactor to use url
+ // Find the links that are localized alternates to the inputPath argument
+ eleventyConfig.addFilter(options.filters.links, function (urlOverride) {
+ let url = urlOverride || this.page?.url;
+ return (contentMaps.localeUrlsMap[url] || []).filter((entry) => {
+ return entry.url !== url;
+ });
+ });
+
+ // Returns a `page`-esque variable for the root default language page
+ // If paginated, returns first result only
+ eleventyConfig.addFilter(
+ "locale_page", // This is not exposed in `options` because it is an Eleventy internals filter (used in get*CollectionItem filters)
+ function (pageOverride, languageCode) {
+ // both args here are optional
+ if (!languageCode) {
+ languageCode = options.defaultLanguage;
+ }
+
+ let page = pageOverride || this.page;
+ let url; // new url
+ if (contentMaps.localeUrlsMap[page.url]) {
+ for (let entry of contentMaps.localeUrlsMap[page.url]) {
+ if (entry.lang === languageCode) {
+ url = entry.url;
+ }
+ }
+ }
+
+ let inputPath = LangUtils.swapLanguageCode(page.inputPath, languageCode);
+
+ if (
+ !url ||
+ !Array.isArray(contentMaps.inputPathToUrl[inputPath]) ||
+ contentMaps.inputPathToUrl[inputPath].length === 0
+ ) {
+ // no internationalized pages found
+ return page;
+ }
+
+ let result = {
+ // // note that the permalink/slug may be different for the localized file!
+ url,
+ inputPath,
+ filePathStem: LangUtils.swapLanguageCode(page.filePathStem, languageCode),
+ // outputPath is omitted here, not necessary for GetCollectionItem.js if url is provided
+ __locale_page_resolved: true,
+ };
+ return result;
+ },
+ );
+}
+
+export { Comparator, LangUtils };
+
+Object.defineProperty(eleventyI18nPlugin, "eleventyPackage", {
+ value: "@11ty/eleventy/i18n-plugin",
+});
+
+Object.defineProperty(eleventyI18nPlugin, "eleventyPluginOptions", {
+ value: {
+ unique: true,
+ },
+});
+
+export default eleventyI18nPlugin;