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 to initialize a non-singleton i18n client. #179

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Tal500
Copy link

@Tal500 Tal500 commented Apr 28, 2022

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 Tal500 marked this pull request as ready for review April 28, 2022 19:25
@kaisermann
Copy link
Owner

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

@Tal500 Tal500 mentioned this pull request Apr 30, 2022
This option tells whether the client document language should
 be changed to the locale, every time it is set.
@Tal500
Copy link
Author

Tal500 commented May 31, 2022

In the second commit I did (842c558), 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>

@kaisermann
Copy link
Owner

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 🙏

@Tal500
Copy link
Author

Tal500 commented Sep 11, 2022

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
Copy link
Author

Tal500 commented Sep 11, 2022

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
Copy link
Author

Tal500 commented Sep 11, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants