@saykit/react 0.0.0-beta-20260911005306 → 0.0.0-beta-20261017001906

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,30 +12,66 @@ A `<Say>` component for rendering translated content in server and client compon
12
12
  pnpm add @saykit/react saykit
13
13
  ```
14
14
 
15
- You will also need a SayKit build-tool plugin and a `saykit.config.ts`.
15
+ You will also need a SayKit build-tool plugin ([`unplugin-saykit`](https://github.com/k0d13/saykit/tree/main/packages/plugin-unplugin) or [`babel-plugin-saykit`](https://github.com/k0d13/saykit/tree/main/packages/plugin-babel)) and a `saykit.config.ts` with `@saykit/transform-jsx` in the bucket. The lazy `() => import(...)` catalogues below need `unplugin-saykit` or `babel-plugin-saykit` with `catalogues: 'module'`; Babel's default inline mode only handles static imports.
16
16
 
17
17
  ## Usage
18
18
 
19
- ```tsx
20
- import { Say } from '@saykit/react';
21
- import { SayProvider } from '@saykit/react/client';
19
+ ```ts title="src/i18n.ts"
22
20
  import { createCatalogue, createStore } from 'saykit';
23
21
 
24
- const en = { greeting: 'Hello, {name}!' };
25
- const fr = { greeting: 'Bonjour, {name} !' };
22
+ export const catalogue = createCatalogue({
23
+ en: () => import('./locales/en.po'),
24
+ fr: () => import('./locales/fr.po'),
25
+ });
26
+
27
+ const initial = catalogue.match(navigator.languages);
28
+ await catalogue.load(initial);
29
+
30
+ export const store = createStore(catalogue, initial);
31
+ export type Locale = (typeof catalogue.locales)[number];
32
+ ```
33
+
34
+ ```tsx title="src/app.tsx"
35
+ import { Say } from '@saykit/react';
36
+ import { SayProvider, useSay } from '@saykit/react/client';
37
+ import { type Locale, store } from './i18n.js';
38
+
39
+ function Cart({ name, items }: { name: string; items: string[] }) {
40
+ return (
41
+ <p>
42
+ <Say>Hello, {name}!</Say>{' '}
43
+ <Say.Plural
44
+ _={items.length}
45
+ _0="Your cart is empty."
46
+ one="You have 1 item."
47
+ other={<>You have {items.length} items.</>}
48
+ />
49
+ </p>
50
+ );
51
+ }
26
52
 
27
- const store = createStore(createCatalogue({ en, fr }), 'fr');
53
+ function LocalePicker() {
54
+ const say = useSay();
55
+ return (
56
+ <select value={say.locale} onChange={(event) => store.set(event.target.value as Locale)}>
57
+ <option value="en">English</option>
58
+ <option value="fr">Français</option>
59
+ </select>
60
+ );
61
+ }
28
62
 
29
- function App() {
63
+ export function App() {
30
64
  return (
31
65
  <SayProvider store={store}>
32
- <Say>Hello, {name}!</Say>
33
- <Say.Plural _={count} one={<>{count} item</>} other={<>{count} items</>} />
66
+ <LocalePicker />
67
+ <Cart name="Ada" items={[]} />
34
68
  </SayProvider>
35
69
  );
36
70
  }
37
71
  ```
38
72
 
73
+ Elements inside a message survive translation as numbered tags: `<Say>Read the <a href="/docs">docs</a></Say>` extracts as `Read the <0>docs</0>`.
74
+
39
75
  ## Documentation
40
76
 
41
77
  [React integration guide](https://saykit.js.org/integrations/react) at [saykit.js.org](https://saykit.js.org).
package/dist/client.d.mts CHANGED
@@ -17,14 +17,17 @@ type SayProviderProps = {
17
17
  store: Store;
18
18
  locale?: never;
19
19
  messages?: never;
20
+ options?: never;
20
21
  } | {
21
22
  store?: never;
22
23
  locale: string;
23
24
  messages: View.Messages;
25
+ options?: View.Options;
24
26
  } | {
25
27
  store?: never;
26
28
  locale?: never;
27
29
  messages?: never;
30
+ options?: never;
28
31
  };
29
32
  /**
30
33
  * Provide a {@link View} to descendant **client** components via context.
@@ -36,8 +39,10 @@ type SayProviderProps = {
36
39
  * locale by the server
37
40
  * @param props.messages The messages for that locale, which should be
38
41
  * referentially stable rather than a fresh object literal per render
42
+ * @param props.options How that locale's view formats its messages, as the
43
+ * server's view was built with
39
44
  */
40
- declare function SayProvider({ store, locale, messages, children }: PropsWithChildren<SayProviderProps>): import("react").FunctionComponentElement<import("react").ProviderProps<any>>;
45
+ declare function SayProvider({ store, locale, messages, options, children }: PropsWithChildren<SayProviderProps>): import("react").FunctionComponentElement<import("react").ProviderProps<Store<string> | null>>;
41
46
  /**
42
47
  * Get the current {@link View}, on the client.
43
48
  * Must be called within a {@link SayProvider}.
package/dist/client.mjs CHANGED
@@ -4,15 +4,17 @@ import { createCatalogue, createStore } from "saykit";
4
4
  //#region src/runtime/client.ts
5
5
  const SayContext = createContext(null);
6
6
  SayContext.displayName = "SayContext";
7
- function SayProvider({ store, locale, messages, children }) {
7
+ function SayProvider({ store, locale, messages, options, children }) {
8
+ const bidiIsolation = options?.bidiIsolation;
8
9
  const held = useMemo(() => {
9
10
  if (store) return store;
10
11
  if (locale === void 0 || !messages) throw new Error("'SayProvider' must be given a store, or a locale and its messages");
11
- return createStore(createCatalogue({ [locale]: messages }), locale);
12
+ return createStore(createCatalogue({ [locale]: messages }, { bidiIsolation }), locale);
12
13
  }, [
13
14
  store,
14
15
  locale,
15
- messages
16
+ messages,
17
+ bidiIsolation
16
18
  ]);
17
19
  return createElement(SayContext.Provider, { value: held }, children);
18
20
  }
@@ -6,10 +6,10 @@ import "server-only";
6
6
  *
7
7
  * A store is a live object and cannot cross the server/client boundary, and a
8
8
  * server component cannot hand its own scope to a client one either. What can
9
- * cross is the locale and its messages, so this reads them off the view the
10
- * the enclosing segment established with {@link import('./server.js').setSay}, and passes them
11
- * to the real provider, which is why `<SayProvider>` written on the server
12
- * takes no props.
9
+ * cross is the locale, its messages and the view's options, so this reads them
10
+ * off the view the enclosing segment established with
11
+ * {@link import('./server.js').setSay}, and passes them to the real provider,
12
+ * which is why `<SayProvider>` written on the server takes no props.
13
13
  */
14
14
  declare function SayProvider({ children }: {
15
15
  children?: ReactNode;
@@ -7,7 +7,8 @@ function SayProvider({ children }) {
7
7
  const say = getSay();
8
8
  return createElement(SayProvider$1, {
9
9
  locale: say.locale,
10
- messages: say.messages
10
+ messages: say.messages,
11
+ options: say.options
11
12
  }, children);
12
13
  }
13
14
  //#endregion
package/dist/index.d.mts CHANGED
@@ -22,10 +22,11 @@ type PropsWithJSXSafeKeys<T> = { [K in keyof T as K extends number | `${number}$
22
22
  * @returns The translation node for the descriptor
23
23
  * @remark This is a macro and must be used with the relevant saykit plugin
24
24
  */
25
- declare function Say(props: PropsWithSayChildren<Disallow<{
25
+ declare function Say(props: PropsWithSayChildren<{
26
+ id?: string;
26
27
  context?: string;
27
28
  whitespace?: boolean;
28
- }, 'id'>>): ReactElement;
29
+ }>): ReactElement;
29
30
  declare namespace Say {
30
31
  /**
31
32
  * Define a pluralised message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/react",
3
- "version": "0.0.0-beta-20260911005306",
3
+ "version": "0.0.0-beta-20261017001906",
4
4
  "description": "React integration for saykit, i18n hooks and components",
5
5
  "keywords": [
6
6
  "i18n",
@@ -53,11 +53,11 @@
53
53
  "jsdom": "^29.1.1",
54
54
  "react": "^19.2.8",
55
55
  "react-dom": "^19.2.8",
56
- "saykit": "^0.0.0-beta-20260911005306"
56
+ "saykit": "^0.0.0-beta-20261017001906"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": "*",
60
- "saykit": "0.0.0-beta-20260911005306"
60
+ "saykit": "0.0.0-beta-20261017001906"
61
61
  },
62
62
  "scripts": {
63
63
  "check": "tsc --noEmit",