11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.

Home Page:https://www.11ty.dev/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

i18n: Can't get localized URL for current language using `locale_url`

danburzo opened this issue · comments

Operating system

macOS Ventura 13.2

Eleventy

2.0.0-beta.3

Describe the bug

With a basic i18n setup and the default permalink schema, an error is thrown when attempting to use locale_url on an URL that contains the default language code in it.

File structure:

src/
  en/
    my-article.md
  ro/
    my-article.md
  it/
    my-article.md

Template file:

<h1>{{ title }}</h1>
{{ content | safe }}
<a href="{{ '/en/my-article/' | locale_url }}">Permalink</a>

Config file:

const { EleventyI18nPlugin } = require('@11ty/eleventy');

module.exports = config => {

	config.addPlugin(EleventyI18nPlugin, {
		defaultLanguage: 'en'
	});

	return {
		dir: {
			input: 'src',
			output: 'dist'
		},
		markdownTemplateEngine: 'njk',
		dataTemplateEngine: 'njk',
		htmlTemplateEngine: 'njk'
	};
};

Reproduction steps

  1. Clone the reproduction repro, navigate to basic-langcode and npm install
  2. Run npm run build

The following error is displayed:

[11ty] 1. Having trouble writing to "dist/en/my-article/index.html" from "./src/en/my-article.md" (via EleventyTemplateError)
[11ty] 2. (./src/_includes/base.njk)
[11ty]   Error: Localized file for URL /en/en/my-article/ was not found in your project. A non-localized version does exist—are you sure you meant to use the `locale_url` filter for this? You can bypass this error using the `errorMode` option in the I18N plugin (current value: "strict"). (via Template render error)

Reproduction URL

https://github.com/danburzo/eleventy-i18n-repro/tree/master/basic-langcode

One possible fix for it would be to check if the page already has the correct language code in the locale_url filter:

// Already has the correct language code
if (Comparator.urlHasLangCode(url, langCode)) {
  return url;
}

I've added a second test case, default-langcode-omitted, which coincidentally also gets fixed with the change above. This case tests whether we can omit the language prefix for the default language, with this permalink setup for the en/ folder:

/* en/en.11tydata.js */
module.exports = {
	/*
		With English being the default language,
		remove the `/en/` prefix from all permalinks.
	*/
	permalink: data => {
		const stem = data.page.filePathStem.replace(/^\/en\//, '');
		const ext = data.page.outputFileExtension;
		if (stem === 'index' || stem.match(/\/index$/)) {
			return `${stem}.${ext}`;
		}
		return `${stem}/index.${ext}`;
	}
};

In the docs, this arrangement is handled with server-side redirects, but there's nothing much in the way of doing it in Eleventy directly.

Before the fix, the template:

<a href="{{ '/my-article/' | locale_url }}">Permalink</a>

Here's the before-fix / after-fix for dist/my-article/index.html, for the English locale:

<!-- before fix, `de` locale is picked up for English -->
<a href="/de/my-article/">Permalink</a>

<!-- after fix, correct URL -->
<a href="/my-article/">Permalink</a>

I say coincidentally fixed because I noticed some weird things, such as:

Comparator.isLangCode('my-article'); // => true

For now I've settled on a pared-down implementation for the i18n plugin that:

  • requires an explicit set of locales to look for in inputPaths, to avoid overidentifying path segments as locales
  • matches pages across languages via normalized inputPath where every locale is replaced with the same string (e.g. ":locale:")
  • includes all locales in locale_links (as proposed in #2789), because it's easier to filter out current locale than to splice it in.
  • doesn't yet implement locale_page as I'm not using any related functionality, and Pagination probably needs some attention
const path = require('path');

module.exports = function EleventyPlugin(config, opts = {}) {
	let options = {
		defaultLocale: 'en',
		locales: ['en'],
		...opts
	};

	let byCanonicalPath = {};
	let byUrl = {};
	config.on('eleventy.contentMap', function (map) {
		Object.entries(map.urlToInputPath)
			.map(function (entry) {
				let locale;
				return {
					url: entry[0],
					canonicalPath: entry[1]
						.split(path.sep)
						.map(function (seg) {
							if (options.locales.includes(seg)) {
								if (!locale) locale = seg;
								return ':locale:';
							}
							return seg;
						})
						.join(path.sep),
					lang: locale || options.defaultLocale,
					label: `TODO[${locale}]`
				};
			})
			.forEach(function (entry) {
				if (!byCanonicalPath[entry.canonicalPath]) {
					byCanonicalPath[entry.canonicalPath] = {};
				}
				byCanonicalPath[entry.canonicalPath][entry.lang] = entry;
				byUrl[entry.url] = entry;
			});
	});

	config.addFilter('locale_url', function (url, overrideLang) {
		const lang = overrideLang || this.page?.lang || options.defaultLocale;
		const canonicalPath = byUrl[url].canonicalPath;
		return byCanonicalPath[canonicalPath][lang]?.url || url;
	});

	config.addFilter('locale_links', function (url) {
		const canonicalPath = byUrl[url].canonicalPath;
		return Object.values(byCanonicalPath[canonicalPath]);
	});
};