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
- Clone the reproduction repro, navigate to
basic-langcode
andnpm install
- 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 ininputPath
s, 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]);
});
};