Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow non-singelton locale initialization, for supporting multi-language&SSR&async server #165

Open
Tal500 opened this issue Oct 11, 2021 · 5 comments
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Comments

@Tal500
Copy link

Tal500 commented Oct 11, 2021

The Problem
The initialization of svelte-i18n sets the preferred language in a global variable.
While this is acceptable for client, it is not acceptable for the server (with SSR).

The server might be async, in which it can proccess at the same time both request for one language and a request for other language.
However, as far as I can see, setting a new locale will override the existing one, and a race condition might occur.

Solution?
Allow to configure the current locale per request and not only globally.

Alternatives
We may instead just run multiply instances of the nodejs server, or just decide that the server render in SSR stage only a fixed language.

Is there already any mechanism to fix it? I'm using Sapper BTW.

@kaisermann
Copy link
Owner

Hey @Tal500 👋

This one's a long coming one. Currently, this is indeed not supported and will require some refactoring. Mainly, we will need to change how we get the stores, moving them inside some factory method.

What I imagine is something like:

Initializing:

// src/i18n.ts
+import { createI18nClient } from 'svelte-i18n'

+const i18n = createI18nClient(options)

+const { t, locale, formatDate, ... } = i18n

+export { t, locale, formatDate, ... }

Using:

-import { t } from 'svelte-i18n'
+import { t } from 'src/i18n.ts'

We could still support importing directly from the module for the majority of use cases, if folks think it's important, by having something like

// src/i18n.ts
import { createi18nClient, setClient } from 'svelte-i18n'

const i18n = createi18nClient(options)

const { t, locale, formatDate, ... } = i18n

+setClient(i18n)

export { t, locale, formatDate, ... }

Note: this is not implemented

@kaisermann kaisermann added enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed labels Oct 11, 2021
@Tal500
Copy link
Author

Tal500 commented Oct 11, 2021

Thanks @kaisermann !

I should notice that after a little bit of checking I'm not sure Svelte is even capable to handle async requests.

In sapper, the middleware return a non-async function, so ExpressJS/Polka will only process one request at a time. (tested in Polka by me)

But as I said, I'm not sure that Svelte can handle async at SSR.

@Tal500
Copy link
Author

Tal500 commented Dec 31, 2021

Thanks @kaisermann !

I should notice that after a little bit of checking I'm not sure Svelte is even capable to handle async requests.

In sapper, the middleware return a non-async function, so ExpressJS/Polka will only process one request at a time. (tested in Polka by me)

But as I said, I'm not sure that Svelte can handle async at SSR.

Well, turned up I was completely wrong (and my previous testing was wrong as well)
It seems that my feature request is critical, and I'll explain.
I will talk only about Sapper because this is what I know, but I believe that it is the same for SvelteKit.

Sapper is a middleware for Express/Polka.
In short, the architecture of Express/Polka middleware is a list of middleware, that call the function next(req, res, next) when they wish to continue HTTP request handaling, and call res.end(...) if this middleware decides the user should get its response, and end the request handling chain.
The reason I'm telling this is for showing that all the process of sapper prefetch/session/whatever is non-blocking(due to the behavior of calling next()), and when it call for a blocking resource it will (assuming the developer using Svelte&Sapper used async process calls) not block, so Express/Polka could process another HTTP request(s) meanwhile, while async-waiting for resources.

The fact that Express/Polka could (and in reality for a production website, will!) process many request at a time should worry you about the singleton global initialization of svelte-i18n!
The problem is, as seen in the example of Sapper with i18 (specifically in this code), that in every request it will override the initialized locale, but since it maybe concurate, the SSR might gives translation of a different language to the end-user.

I believe it will be fixed after Svelte will be loaded at the client code, but still it is very disturbing to know it might (and will) happen!

Do you agree that this issue is very serious @kaisermann? I believe that anyone that use it for production shell execute different instance of Sapper/SvelteKit servers by each locale, and redirect request to the correct server by reading the locale cookie in HTTP request.

@Tal500
Copy link
Author

Tal500 commented Jan 31, 2022

Hi @kaisermann, here is my suggestion according to your preferred syntax. First, it should keep support for the currently support syntax, and add a new recommended syntax for future code.

As you said, we need to have some "i18n Client" object, probably represented directed by the funcs { _, locale, formatDate, ... }. We also need that Svelte users could easily translate import { _, locale, formatDate, ... } from 'svelte-i18n'; to { _, locale, formatDate, ... } = i18n;.

Additionally, I think we need to take advantage of Svelte context management functions functions (available only during component initialization).

Proposed User Usage

The user can write the initialization code in src/i18n.js:

// src/i18n.js
import { register, createI18nClient } from 'svelte-i18n';

register('en', () => import('./en.json'));
register('en-US', () => import('./en-US.json'));
register('pt', () => import('./pt.json'));

function setupI18nClient(locale?) {
    const client = createI18nClient({
        fallbackLocale: 'en',
        initialLocale: locale,
    });

    return client;
}

And in the main Svelte component (App.svelte in the examples, or src/routes/_layout.svelte in Sapper/SvelteKit) write this:

// Main Svelte component (e.g. App.svelte or src/routes/_layout.svelte)
...
<script>
...
    import { getLocaleFromNavigator, setI18nClientInContext } from 'svelte-i18n';
    import { setupI18nClient } from '../i18n';

    const i18nClient = setupI18nClient(getLocaleFromNavigator());
    setI18nClientInContext(i18nClient);// Set i18n Client in Svelte context
...
</script>
...

If the user use SSR, she may pass the locale data in session data, and wait for locale dictionary loading before returning.
In Sapper (and very similarly, in SvelteKit), one should seed in session data the correct value of the locale by reading a cookie, and wait for locale dictionary loading in preload.
Then src/routes/_layout.svelte could look like this:

// src/routes/_layout.svelte

<script context="module">
    import { waitLocale } from 'svelte-i18n';

    export async function preload(page, session) {
        await waitLocale(session.locale);
    }
</script>

<script>
...
    import { get } from 'svelte/store';
    import { stores } from '@sapper/app';
    import { getLocaleFromNavigator, setI18nClientInContext } from 'svelte-i18n';
    import { setupI18nClient } from '../i18n';

    const { session } = stores();
    const locale = (typeof window !== 'undefined') ? getLocaleFromNavigator() : get(session).locale;

    const i18nClient = setupI18nClient(locale);
    setI18nClientInContext(i18nClient);// Set i18n Client in Svelte context

    if (typeof window !== 'undefined') {
        // TODO: Set here a cookie in client side with the value of locale=getLocaleFromNavigator()
    }
...
</script>
...

Now all components can use formatting this way (including 3rd parties):

// Some Svelte component
...
<script>
...
    import { getI18nClientFromContextOrGlobal } from 'svelte-i18n';

    { _, locale, formatDate } = getI18nClientFromContextOrGlobal();
...
</script>
...

The function getI18nClientFromContextOrGlobal() returns the current i18n that is on the context, or if none on the context returns the global i18n (the "old" singleton i18n initialization), or throw an exception if none of them did happen. (See implementation details)
The reason for returning the global singleton is that 3rd party code could easily support both "new" and the "old" approach of svelte-i18n.

Implementation details

I looked over the directory structure of svelte-18n code. The tough thing require a major refactoring is implementing the function createI18nClient() to return JS object { _, locale, formatDate, ... }. I think no one but the maintainer @kaisermann could perform or at least instruct how to modify the code&directory structure.
Again, as I explained in my previous comment, this feature is not only "nice to have", it is critical if you wish to support i18n in a production environment with SSR, or otherwise you'd have race condition (meaning, at least, bad/mixed HTML language translation in server HTTP response).

I will speak about how to implement the rest of the functions, that's easy:

// Internal code of svelte-i18n
import { setContext, getContext } from 'svelte';

const key = {};

// All the functions below can be called only in Svelte component initialization

export function setI18nClientInContext(i18nClient) {
    setContext(key, i18nClient);
}

export function clearI18nClientInContext() {
    setContext(key, undefined);
}

export function getI18nClientFromContextOrGlobal() {
    var i18nClient = getContext(key);

    if (i18nClient) {
        return i18nClient;
    } else {
        // Return the global singleton client that was initialized by 'init()', or throw an exception if none.
    }
}

What do you think?
I think I'm following @kaisermann suggestion, just use additionally the power of Svelte contexts for the user sanity to handle i18n client managing.

@mulder999
Copy link

mulder999 commented Oct 29, 2023

To address SSR (Server-Side Rendering) generation, you can follow this approach in your Svelte application:

Call waitLocale function from 'svelte-i18n' to ensure the desired locale is loaded in cache before rendering the page:

#In [[lang]]/+page.ts
import { waitLocale } from 'svelte-i18n';

export async function load({ params }): Promise<Record<string, string>> {
    const lang = params.lang || "en";
    await waitLocale(lang);
    return {
        lang
    };
}

Pass the $page.data.lang to the formatting methods, for eg:

# Any svelte component
<script lang="ts">
    import { _, json } from "svelte-i18n";
    import { page } from "$app/stores";
</script>

{$_("box.readmore", {locale: $page.data.lang})}
OR
{$json("box.readmore", $page.data.lang)}

Control that you never set the locale store, nor rely on it in your application.

This is enough for SSR. But since we are not using the local store anymore, you can add the code below in your main +layout.svelte component to restore the reactive update of the 'lang' attribute of the HTML document during client-side rendering (CSR):

#+layout.svelte
...
$: if (browser) document.documentElement.setAttribute("lang", $page.data.lang);
...

I think it would be advisable to revise the SSR example in the documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants