Skip to content

Internationalizing URLs

Internationalizing URL paths is possible with the same conveniences of no/minimal code changes, while respecting the fact that URLs are to be handled carefully.

There are two parts to this:

  • Translation: e.g. /about to /acerca-de
  • Localization: e.g. /about to /en/about

And both are supported and you can combine them into: /es/acerca-de.

You first specify the patterns that should be internationalized in the config. Since wuchale uses path-to-regexp for the actual handling of the patterns, you can use any syntax supported by it.

wuchale.config.js
export default {
locales: ['en', 'es'],
adapters: {
main: svelte({
loader: 'sveltekit',
url: {
patterns: [
'/about',
'/items{/*rest}',
'/',
'/*rest',
],
// you can also pass a filename that supports a localize function
localize: true,
},
}),
}
// ...
}

The paths that don’t match these patterns are ignored, so you can choose which ones you want to internationalize.

Now, what happens next is two things.

The first is that the patterns themselves are put inside the catalog storage (e.g. .po) files so that they are translated by the translator. This makes it consistent that all translation can be handled by the translator. But URLs patterns should be handled carefully. Therefore, by default, the pofile handler puts them in a separate *.url.po file, like this:

es.po
#: main
#, url:main,url:js
msgctxt "original: /items{/*rest}"
msgid "/items{0}"
msgstr "/elementos{0}"
  • It sets the url:{adapterKey} flags to convey that they are used as URL patterns by these adapters
  • To prevent accidental translation of parameters which could cause problems when they are used, it converts them into its own placeholder notation
  • But it puts the original pattern in the context above it to
    • give more context to the translator and
    • distinguish between patterns that would become the same after the params are converted into {0}

Then, after the translation, it generates a URL manifest file and a corresponding file, urls.js in the locales directory, which exports a function to match URLs during routing.

During routing, wuchale has to reverse the two parts (translation and localization) to get the original path and route it correctly. And the created manifests and urls.js files are handy for this. For example, to use the matching from the above config in SvelteKit’s hooks.js:

hooks.js
import { matchUrl } from './locales/single.url.js'
import { deLocalizeDefault } from 'wuchale/url'
import { locales } from './locales/data.js'
/** @type {import('@sveltejs/kit').Reroute} */
export const reroute = ({url}) => {
// get /acerca-de and es from /es/acerca-de
const [upath, locale] = deLocalizeDefault(url.pathname, locales)
// get /about from /acerca-de
const {path} = matchUrl(upath, locale)
// return the original in case no pattern matches
return path ?? url.pathname
}

The other part is this, when it gets a text fragment that is matched as a URL, it translates it from the pattern translation so that the it is translated exactly the same way as the routing and prevent 404 errors.

Let’s take the following example:

<a href="/items/foo/{itemId}">Item</a>

It checks if it matches any of the patterns, in this case this matches items{/*rest}. It then:

  • Gets the translated pattern, /elementos{/*rest}
  • Replaces the parameter to make it like any other message, /elementos/foo/{0}
  • Compiles and includes it in the compiled catalog
  • Transforms the code to use the compiled value, and wraps it with the configured localize function

All of this is done once, during build time. After after that, the URLs are just like other strings, static or interpolated, just included in one big array.

The localization part is project-dependent. Since the most common way is to prefix the locale in front of the path like /en/about, wuchale automatically does that when you set localize: true in the URL config. But that may not be enough, for example if:

  • The site uses different domain names for different locales, and/or
  • Whether a locale needs to be prefixed is dependent on the domain if the app targets multiple domains

Especially in the second case, for example let’s say we have an /about page with example.com and example.es as targets. And we want to have both en and es locales on both, but we don’t want unnecessary locale prefixes so we want:

  • example.com/about
  • example.es/acerca-de
  • example.com/es/acerca-de
  • example.es/en/about

Cases like this cannot be fulfilled with simple prefixing. In that case, you can implement both the localization and de-localization. For the former, you can export your own localize function from any file, provide the file to the config:

wuchale.config.js
export default {
// ...
adapters: {
main: svelte({
// ...
url: {
patterns: [
// ...
],
localize: 'src/lib/url.js',
},
}),
}
// ...
}

So it would roughly be like

src/lib/url.js
export function localize(path, locale) {
const host = (new URL(location.href)).host
if (host === 'example.com' && locale === 'en' || host === 'example.es' && locale === 'es') {
return path
}
return `/${locale}/${path}`
}

As for the de-localization, since you handle that at the routing level, and wuchale just provided with a util for the default, you don’t configure anything. You just apply a matching implementation at the right place (e.g. SvelteKit hooks.js):

import { matchUrl } from './locales/single.url.js'
import { deLocalize } from './lib/url.js' // just an example
import { locales } from './locales/data.js'
/** @type {import('@sveltejs/kit').Reroute} */
export const reroute = ({url}) => {
const [upath, locale] = deLocalize(url.pathname, locales)
const {path} = matchUrl(upath, locale)
return path ?? url.pathname
}

The implementation of deLocalize is left as an exercise.

Most of the time, when the locale in a page is changed (using a drop-down for example), it is desired to go to the corresponding page in the new locale instead of jumping to the home page. To achieve this, the current path must be translated to the new locale. The steps are as follow:

  1. DeLocalize the path to remove any prefixes
  2. Extract the dynamic params for that path and get the alternate patterns
  3. Fill the params on the pattern of the desired new locale
  4. Localize the new path with the new locale

wuchale provides the utilities to do these easily. For example, with the default localization:

src/lib/utils.js
import {deLocalizeDefault, fillParams, localizeDefault} from "wuchale/url"
import {matchUrl} from "../locales/main.url.js"
/**
* @param {string} url
* @param {import("../locales/data.js").Locale} locale
*/
function translateUrl(url, locale) {
const [pathOnly, prevLocale] = deLocalizeDefault(url, locales);
const result = matchUrl(pathOnly, prevLocale)
if (result.path !== null) {
const targetPath = fillParams(result.params, result.altPatterns[locale])
return localizeDefault(targetPath, locale)
}
return localizeDefault(url, locale)
}

If you use other ways of localization/de-localization, you can adapt it by changing the corresponding functions.