Skip to content

Using EN on non-user surfaces with non-EN base locale for user surfaces #554

@fr33bits

Description

@fr33bits

This is both a showcase of my implementation, feature request and a question if the following can be implemented in a better way.

I have a Svelte(Kit) app within which I use Paraglide JS with the strategies cookies, preferredLanguage and baseLocale. However the Paraglide JS code also ends up getting used in non-user facing surfaces of the app (for example because it is used to localize a custom Zod error) such as the API and various developer (package.json) scripts where we want everything to use the EN locale. The issue is that the baseLocale for the user facing part of the app should really be a non-EN locale as that is the primary locale of the app, with localization into other locales a nice extra.

The question is then how to achieve that non-user surfaces use EN as the locale while a non-EN locale is used as a fallback for user surfaces (meaning the actual web app UI).

API

The API shares some error message code with the client where the messages are supposed to be localized. However since cookies may be carried alongside an API request (because the API logic exists in the same SvelteKit context as the user-facing part of the app), the server may pick up a non-EN locale from the request and carry it

I managed to force the API to always use the EN locale by writing a custom server strategy custom-apiStrategy and put it at the top of the list of strategies so that it gets evaluated first. I placed the strategy in the handleParaglide() hook before the return paraglideMiddleware():

defineCustomServerStrategy('custom-apiStrategy', {
	getLocale: (request) => {
		if (!request) {
			logger.error('Undefined request in custom-apiStrategy');
			return;
		}

		const url = new URL(request.url);
		if (url.pathname.startsWith(apiPath)) {
			return 'en';
		}
	}
});

This works great and sets EN as the appropriate locale for all /api/* requests

Non-user surfaces outside the Svelte(Kit) context

A bigger challenge has been getting the same to work on non-user surfaces outside of the Svelte(Kit) context, such as env var validation scripts that rely on a Zod schema that has an localized error message (which is standardized and also gets used in user-facing parts of the app).

Setting EN as locale on non-user surfaces

I have tried setting baseLocale to the actual non-EN base locale for user surfaces and then telling Paraglide JS when it should be operating on a non-user surface where the locale is EN. I tried achieving the latter through setLocale('en') or overwriteGetLocale(() => 'en') but neither of those worked and Paraglide just used the non-EN baseLocale.

This is despite the fact that the script was run using vite-node and otherwise should've had access to the Vite context (AFAIK).

User surfaces base locale

Therefore, I took an alternative approach where I set baseLocale to EN and endeavored to devise custom strategies that would serve as a user-facing base locale if all other strategies failed. Since there is only a single user-facing surface and potentially many surfaces which are not, this has an advantage of only having to set the correct locale once instead of for every non-user surface.

Although in most cases, the appropriate locale will be obtained from cookies and preferredLanguage, I cannot rely on them in all cases. cookies are perhaps not set because the browser does not have JavaScript enabled or they are not yet visible server-side on the first page visit (but would be after refresh). preferredLanguage may return undefined if the no browser-signaled locales match the locales the app supports.

Furthermore, the app obtains localized data (through a load function of course) from the server, which must match the locale the UI appears in. I had to spend some time debugging to make sure this was actually the case, especially on first load (when no cookie was set). The locale of the data that will be returned is selected based on the getLocale() value, so it must be appropriately set on the server as well.

So I created a custom strategy that will detect if it exists on a user surfaces and in that case return userSurfacesBaseLocale as the locale.

First, the client-side code:

defineCustomClientStrategy('custom-userSurfacesBaseLocale', {
	getLocale: () => {
		return userSurfacesBaseLocale;
	},
	setLocale: () => {
		// Locale not meant to be set through this strategy as it is a fallback
	}
});

However this requires JavaScript to run and may load data in an incorrect locale, at least on first visit. Therefore a server-side implementation is necessary as well:

defineCustomServerStrategy('custom-userSurfacesBaseLocale', {
	// defined on the server because it does not require JavaScript to work (useful for noscript messages) and would otherwise initially fetch the database data using the baseLocale
	getLocale: (request) => {
		if (!request) {
			logger.error('Undefined request in custom-userSurfacesBaseLocale');
			return;
		}

		// on the server, custom strategies take precedence over the usual strategy order
		// if those are viable, they need to be evaluated first so this strategy does not override them
		const cookieLocale = event.cookies.get(cookieName);
		const headerLocale = extractLocaleFromHeader(request);
		if (cookieLocale || headerLocale) {
			console.log(
				`Valid locale is already set through cookie (${cookieLocale}) or browser header (${headerLocale}), exiting...`
			);
			// a valid locale can be set through strategies that will be evaluated after this one
			return;
		}

		const url = new URL(request.url);
		if (!url.pathname.startsWith(apiPath)) {
			// exclusion of the API non-user surface already handled in 'custom-apiStrategy'
			console.log('custom-userSurfacesBaseLocale has been triggered');
			return userSurfacesBaseLocale;
		}
	}
});

This was placed right after the custom-apiStrategy strategy.

Here is my list of strategies:

strategy: [
	// from highest to lowest priority, except on server where custom strategies take precedence
	'custom-apiStrategy', // API requests use nonUserSurfacesLocale; needs to be before 'cookie' because cookies may be sent with API request and take precedence otherwise
	'cookie', // manually set (through Header component) user locale
	'preferredLanguage', // browser-signaled locale preferences
	// user surface base locale if preferredLanguage fails or its locales not supported
	'custom-userSurfacesBaseLocale',
	'baseLocale' // non-API non-user surfaces (e.g. validation scripts); overwriteGetLocale() and setLocale() don't work there
]

My question is if there is a less cumbersome way of achieving the same effect, particularly for non-user surfaces outside the Svelte(Kit) context? It would be a lot less cumbersome if strategies had the same order of precedence as they do on the client (why is that even a thing?), so it would be good if that could be enabled in a future version, although I suspect there are architectural reasons for why this cannot be.

Have others faced similar issues (I could not find anything) and how did they solve them? Surely such cross-use of Paraglide JS code between user and non-user surfaces is not that uncommon.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions