Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/i18n-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@solid-primitives/i18n": major
---

Migrate to Solid.js v2.0 (beta.10)

## Breaking Changes

**Peer dependency**: `solid-js@^2.0.0-beta.10` is now required.

### `@solid-primitives/i18n`

- `createResource` is removed in Solid 2.0 — use `createMemo` with an async function for dynamic dictionary loading, or a synchronous `createMemo` for static dictionaries
- `Suspense` is replaced by `Loading` from `solid-js` for wrapping async dictionary reads
- `useTransition` is removed — use `isPending()` from `solid-js` to observe transition state
- `onMount` replaced by `onSettled` for post-hydration lifecycle callbacks
- `createEffect` now requires the split compute/apply form: `createEffect(compute, effect)` — single-argument usage is no longer supported
- The `jsx` entry in test dictionaries no longer returns JSX elements; the test helper `setup.tsx` is renamed to `setup.ts` with plain object returns
96 changes: 30 additions & 66 deletions packages/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ const fr_dict: Dict = {

When using large dictionary files, JSON files are [faster to load](https://www.youtube.com/watch?v=ff4fgQxPaO0). Additionally, we recommend keeping a flat JSON structure so you don't need to flatten the object on the client for best performance.

### With `createResource`
### Dynamic loading

Example of using `@solid-primitives/i18n` with `createResource` to dynamically load directories for selected languages.
Use `createMemo` with an async function to load dictionaries on demand. The translator suspends inside a `<Loading>` boundary until the dictionary resolves.

```tsx
import { type Component, createMemo, createSignal, Loading } from "solid-js";
import * as i18n from "@solid-primitives/i18n";

/*
Expand All @@ -67,7 +68,7 @@ Assuming the dictionaries are in the following structure:
en.ts
fr.ts
es.ts
And all exports a `dict` object
And all export a `dict` object
*/

// use `type` to not include the actual dictionary in the bundle
Expand All @@ -85,86 +86,49 @@ async function fetchDictionary(locale: Locale): Promise<Dictionary> {
const App: Component = () => {
const [locale, setLocale] = createSignal<Locale>("en");

const [dict] = createResource(locale, fetchDictionary);

dict(); // => Dictionary | undefined
// (undefined when the dictionary is not loaded yet)

const t = i18n.translator(dict);

t("hello"); // => string | undefined
const dict = createMemo(async () => fetchDictionary(locale()));
const t = i18n.translator(dict, i18n.resolveTemplate);

return (
<Suspense>
<Show when={dict()}>
{dict => {
dict(); // => Dictionary (narrowed by Show)

const t = i18n.translator(dict);

t("hello"); // => string

return (
<div>
<p>Current locale: {locale()}</p>
<div>
<button onClick={() => setLocale("en")}>English</button>
<button onClick={() => setLocale("fr")}>French</button>
<button onClick={() => setLocale("es")}>Spanish</button>
</div>

<h4>{t("hello", { name: "John" })}</h4>
<h4>{t("goodbye", { name: "John" })}</h4>
<h4>{t("food.meat")}</h4>
</div>
);
}}
</Show>
</Suspense>
<Loading fallback={<div>Loading...</div>}>
<div>
<p>Current locale: {locale()}</p>
<div>
<button onClick={() => setLocale("en")}>English</button>
<button onClick={() => setLocale("fr")}>French</button>
<button onClick={() => setLocale("es")}>Spanish</button>
</div>

<h4>{t("hello", { name: "John" })}</h4>
<h4>{t("goodbye", { name: "John" })}</h4>
<h4>{t("food.meat")}</h4>
</div>
</Loading>
);
};
```

### With initial dictionary

Instead of narrowing the current dictionary with `Show`, you can also provide an initial dictionary to `createResource`.

```ts
// en dictionary will be included in the bundle
import { dict as en_dict } from "./i18n/en.js";

const [dict] = createResource(locale, fetchDictionary, {
initialValue: i18n.flatten(en_dict),
});

dict(); // => Dictionary
```

### With transitions

Since the dictionary is a resource, you can use solid's transitions when switching the locale.
Use `isPending()` to show visual feedback while a locale switch is in progress.

```tsx
const [dict] = createResource(locale, fetchDictionary);

const [duringTransition, startTransition] = useTransition();
import { createMemo, isPending, Loading } from "solid-js";

function switchLocale(locale: Locale) {
startTransition(() => setLocale(locale));
}
const dict = createMemo(async () => fetchDictionary(locale()));

return (
<div style={{ opacity: duringTransition() ? 0.5 : 1 }}>
<Suspense>
<div style={{ opacity: isPending() ? 0.5 : 1 }}>
<Loading>
<App />
</Suspense>
</Loading>
</div>
);
```

### Static dictionaries

If you don't need to load dictionaries dynamically, you can use `createMemo` instead of `createResource`.
If you don't need to load dictionaries dynamically, use `createMemo` with a synchronous function.

```tsx
import * as en from "./i18n/en.js";
Expand Down Expand Up @@ -236,12 +200,12 @@ Translations in `root.ts` would be available in all modules. Translations in `lo
// root.ts

const [locale, setLocale] = createSignal<Locale>("en");
const [commonDict] = createResource(locale, fetchCommonDictionary);
const commonDict = createMemo(async () => fetchCommonDictionary(locale()));
const t = i18n.translator(commonDict);

// login/login.ts

const [loginDict] = createResource(locale, fetchLoginDictionary);
const loginDict = createMemo(async () => fetchLoginDictionary(locale()));

// translator only for login module
const loginT = i18n.translator(loginDict);
Expand All @@ -250,7 +214,7 @@ t("welcome"); // => 'Welcome from common translations!'
loginT("welcome"); // => 'Welcome from login translations!'
```

Or combine multiple dictionaries into one. While prefixing the keys with the module name.
Or combine multiple dictionaries into one, prefixing the keys with the module name.

```ts
const combined_dict = createMemo(() => ({
Expand Down
76 changes: 26 additions & 50 deletions packages/i18n/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
type Component,
createResource,
createSignal,
onMount,
Show,
Suspense,
useTransition,
} from "solid-js";
import { type Component, createMemo, createSignal, isPending, Loading, onSettled } from "solid-js";
import * as i18n from "../src/index.js";
import * as en from "./en.js";
import * as es from "./es.js";
Expand All @@ -28,53 +20,37 @@ const App: Component = () => {
const [locale, setLocale] = createSignal<Locale>("en");
const [name, setName] = createSignal("User");

const [dict] = createResource(locale, fetchDictionary);

const [duringTransition, startTransition] = useTransition();

function switchLocale(locale: Locale) {
startTransition(() => setLocale(locale));
}
const dict = createMemo(async () => fetchDictionary(locale()));
const t = i18n.translator(dict, i18n.resolveTemplate);

return (
<Suspense>
<Show when={dict()}>
{dict => {
const t = i18n.translator(dict, i18n.resolveTemplate);

return (
<div class="center-child w-full">
<div
class="my-24 transition-opacity"
classList={{ "opacity-50": duringTransition() }}
>
<p>
Current locale: <b>{locale()}</b>
</p>
<div>
<button onClick={() => switchLocale("en")}>English</button>
<button onClick={() => switchLocale("fr")}>French</button>
<button onClick={() => switchLocale("es")}>Spanish</button>
</div>

<div class="mb-8" />
<button onClick={() => setName(n => (n === "User" ? "Viewer" : "User"))}>
Change Name
</button>
<h4>{t("hello", { name: name() })}</h4>
<h4>{t("goodbye", { name: name() })}</h4>
<h4>{t("food.meat")}</h4>
</div>
</div>
);
}}
</Show>
</Suspense>
<div class="center-child w-full">
<Loading>
<div class={`my-24 transition-opacity${isPending() ? "opacity-50" : ""}`}>
<p>
Current locale: <b>{locale()}</b>
</p>
<div>
<button onClick={() => setLocale("en")}>English</button>
<button onClick={() => setLocale("fr")}>French</button>
<button onClick={() => setLocale("es")}>Spanish</button>
</div>

<div class="mb-8" />
<button onClick={() => setName(n => (n === "User" ? "Viewer" : "User"))}>
Change Name
</button>
<h4>{t("hello", { name: name() })}</h4>
<h4>{t("goodbye", { name: name() })}</h4>
<h4>{t("food.meat")}</h4>
</div>
</Loading>
</div>
);
};

export default function () {
const [is_mounted, set_mounted] = createSignal(false);
onMount(() => set_mounted(true));
onSettled(() => set_mounted(true));
return <>{is_mounted() && <App />}</>;
}
4 changes: 2 additions & 2 deletions packages/i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
"primitives"
],
"peerDependencies": {
"solid-js": "^1.6.12"
"solid-js": "^2.0.0-beta.10"
},
"devDependencies": {
"solid-js": "^1.9.7"
"solid-js": "2.0.0-beta.10"
}
}
32 changes: 16 additions & 16 deletions packages/i18n/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from "vitest";
import * as i18n from "../src/index.js";
import { createEffect, createResource, createRoot, createSignal } from "solid-js";
import { Locale, en_dict, pl_dict } from "./setup.jsx";
import { createEffect, createMemo, createRoot, createSignal, flush } from "solid-js";
import { Locale, en_dict, pl_dict } from "./setup.js";

describe("dict", () => {
test("flatten", () => {
Expand Down Expand Up @@ -168,37 +168,37 @@ Object.entries({
});

describe("reactive", () => {
test("with translator", async () => {
test("with translator", () => {
const [locale, setLocale] = createSignal<Locale>("en");
let hello = "";
let to_usd = 0;

const dispose = createRoot(dispose => {
const [dict] = createResource(
locale,
async locale => {
const dict = locale === "en" ? en_dict : pl_dict;
return i18n.flatten(dict);
},
{ initialValue: i18n.flatten(en_dict) },
);
const dict = createMemo(() => i18n.flatten(locale() === "en" ? en_dict : pl_dict));

const t = i18n.translator(dict, i18n.resolveTemplate);
const chained = i18n.chainedTranslator(en_dict, t);

createEffect(() => {
hello = t("hello", { name: "Tester", thing: "day" });
to_usd = chained.data.currency["to.usd"]();
});
createEffect(
() => ({
hello: t("hello", { name: "Tester", thing: "day" }),
to_usd: chained.data.currency["to.usd"](),
}),
({ hello: h, to_usd: u }) => {
hello = h;
to_usd = u;
},
);

return dispose;
});

flush();
expect(hello).toBe("Hello Tester! How is your day?");
expect(to_usd).toBe(1);

setLocale("pl");
await Promise.resolve();
flush();
expect(hello).toBe("Cześć Tester!");
expect(to_usd).toBe(0.27);

Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/test/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import * as i18n from "../src/index.js";
import { en_dict } from "./setup.jsx";
import { en_dict } from "./setup.js";

describe("template", () => {
test("identity template resolver", () => {
Expand Down
Loading