svelte-i18n icon indicating copy to clipboard operation
svelte-i18n copied to clipboard

Allow to initialize a non-singleton i18n client.

Open Tal500 opened this issue 2 years ago • 6 comments

Intro

Solves #165. One of the reasons this issue was critical is because of serving SSR on two different languages in the same time, but we gain much more by solving this issue.

It was a hard and tough work, but I have managed to do it finally. I have carefully redesign the code to be able (internally) to create new formatters and locales given some options (not necessarily the global ones from getOption()!). Using the above, I could finally implement the function createI18nClient().

Additionally, as I mentioned in the issue, the user may&should use the context based API (calling setupI18nClientInComponentInit()), so any component designer could get the i18n stores via getI18nClientInComponentInit(), see the next example.

Seketch for a Sapper User

client.ts

import * as sapper from '@sapper/app';
import { waitLocale } from 'svelte-i18n';
// ...

const initialLocale = __SAPPER__.preloaded[0].initialLocale ;// A nasty way to get the preloaded initial locale
console.log(`Wait for locale "${initialLocale}"`);

waitLocale(initialLocale).then(() => {
	sapper.start({
		target: document.querySelector('#sapper')
	});
});

i18n.ts (notice to the unresolved problem in fallbackLocale)

import { getContext, hasContext, setContext, onDestroy } from 'svelte';
import { initLifecycleFuncs, setupI18nClientInComponentInit } from 'svelte-i18n';
// ...

initLifecycleFuncs({ hasContext, setContext, getContext, onDestroy });

export function setup_i18n(initialLocale: string) {
	const client = setupI18nClientInComponentInit({
		fallbackLocale: initialLocale,// Must be the same locale, otherwise the messages may not be loaded yet (couldn't fix it).
		initialLocale,
		loadingDelay: 0,
	});

	return { initialLocale, client };
}

src/_layout.svelte

<script context="module" lang="ts">
	import { waitLocale } from 'svelte-i18n';
  
	export async function preload(page, session) {
		const { initialLocale } = session;// Need to seed initial locale in the session (by cookie for example)
		await waitLocale(initialLocale);
		return { initialLocale};
	}
</script>

<script lang="ts">
	import { setup_i18n } from '../i18n';
	// ...
	export let initialLocale: string;
	const { client } = setup_i18n(initialLocale);
	// ...
</script>
// ...

A generic component now will do: (can get more stores if they wish, not just _)

<script lang="ts">
	import { getI18nClientInComponentInit } from "svelte-i18n";
	const { _ } = getI18nClientInComponentInit();
	// Instead of the "deprecated" way: import { _ } from "svelte-i18n";
	// ...
</script>
// ...

Using getI18nClientInComponentInit() is great, because the designer can use svelte-i18n without knowing i18n setup details. This allow the main website developer to choose one of the following, and the component will just work:

  • Keep setup i18n in the "deprecated" way, globally just by using init().
  • Setup i18n in the "global component" which is src/_layout.svelte.
  • Nested localization - the developer might choose for some reason that some sub layout/page/component might need to be displayed in a different language than the main layout. How can he do it? Simply by doing what you see in src/_layout.svelte also the sub layout/page/component (for the component case, one needs to be careful and make sure the the locale were already loaded before the component initialization, since there is no preload() function. All this use cases was verified by me on my private website project.

Tests

Because of lint problems on base commits I just ignored it. But Jest tests works very well, and I enhance them for checking the newly added code of mine.

I was also testing this in my own private svelte website project, as said above.

Tal500 avatar Apr 28 '22 18:04 Tal500

Hey @Tal500 👋thanks for this! I'll give it a good read soon and review it

kaisermann avatar Apr 29 '22 05:04 kaisermann

In the second commit I did (842c55815fb11814c82c8d21c055289735365b6b), I let the user cancel automatic setting of document lang atrribute.

I also would like to share a possible code of Svelte component(not included in the PR), making life easier for nested i18n clients:

<script lang="ts">
  import { onDestroy } from 'svelte';
  import type { Readable, Subscriber } from 'svelte/store';
  import { setupI18nClientInComponentInit } from "svelte-i18n";

  export function componentInitializationSubscribe<T>(store: Readable<T>, run: Subscriber<T>) {
    const unsubscribe = store.subscribe(run);

    onDestroy(() => { unsubscribe(); });
  }

  export let locale: string = undefined;

  const client = setupI18nClientInComponentInit({
    fallbackLocale: locale,// Must be the same locale, otherwise the messages may not be loaded yet.
    initialLocale: locale,
    loadingDelay: 0,
    autoLangAttribute: false,
  });
  const { _, t, json, date, time, number, format, isLoading } = client;

  const localeStore = client.locale;
  componentInitializationSubscribe(localeStore, () => {
    if (locale !== $localeStore) {
      locale = $localeStore;
    }
  });
  $: $localeStore = locale;
</script>

<div lang={locale}><slot client={client} locale={$localeStore} _={$_} t={$t} json={$json} date={$date} time={$time}
  number={$number} format={$format} isLoading={$isLoading}></slot></div>

An example to a user code using it:

<I18nContainer locale="fr" let:_={_}>
{_('test_str')}
<SomeComponent1 />
<SomeComponent2 />
</I18nContainer>

Tal500 avatar May 31 '22 17:05 Tal500

Hey @Tal500! Thanks again for your PR! After giving it a read and some consideration, I think I would implement it a little bit differently. I will probably do it in the coming weeks, but I will make sure to add you as a co-author 🙏

kaisermann avatar Sep 11 '22 14:09 kaisermann

Hey @Tal500! Thanks again for your PR! After giving it a read and some consideration, I think I would implement it a little bit differently. I will probably do it in the coming weeks, but I will make sure to add you as a co-author 🙏

Great! I used this PR as a fork for few months, will glad to see a good official implementation eventually.

Tal500 avatar Sep 11 '22 14:09 Tal500

client.ts

import * as sapper from '@sapper/app';
import { waitLocale } from 'svelte-i18n';
// ...

const initialLocale = __SAPPER__.preloaded[0].initialLocale ;// A nasty way to get the preloaded initial locale
console.log(`Wait for locale "${initialLocale}"`);

waitLocale(initialLocale).then(() => {
	sapper.start({
		target: document.querySelector('#sapper')
	});
});

Additionally, I would like to share an idea about how to detect the correct locales to be loaded on the client-side, after SSR, which are the ones who are being loaded in waitLocale on client initialization.

In the preloading, I'm awaiting to the needed locales to be loaded on SSR. Then, the main preloading method tells the main layout which locale(s) should be loaded. The main layout emit the following HTML in the end of the layout file:

function htmlToAdd() {
	return `<script id=${jsonId} type="application/json">${
		JSON.stringify(Array.from(this.loaded))
	}</script>`
}

So on the client loading, we can get do:

function waitOnClientLoad() {
	const localeList: string[] = JSON.parse(document.getElementById(jsonId).textContent);
	console.log(`Waiting for locales ${JSON.stringify(localeList)}`);
	return Promise.all(localeList.map((locale) => waitLocale(locale)));
}

If you'd be more sophisticated a little bit, with the help of Svelte Context API, you can also add a locale to be loaded not only on the main layout.

This is the way for dealing with Sapper. SvelteKit is similar, except that you should do ES6 top-level await, since you don't have the client initialization script there.

A disadvantage I didn't solve: Except from the fact it's complicated, another issue is that you'd prefer to load the translation scripts(i.e. the compiled JSON) as <script> tags on SSR, instead of first loading the entry script first and only just then tell the browser to load the localization scripts(i.e. the compiled JSON). It is somehow related to the issue I had opened on SvelteKit - sveltejs/kit#6655.

Tal500 avatar Sep 11 '22 14:09 Tal500

BTW, I'd like to here about the sketch of your plans for the change in the library. I might have suggestions

Tal500 avatar Sep 11 '22 17:09 Tal500