@saykit/react 0.11.1 → 0.12.0

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,7 +12,7 @@ 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 ([`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.
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.
16
16
 
17
17
  ## Usage
18
18
 
@@ -20,8 +20,8 @@ You will also need a SayKit build-tool plugin ([`unplugin-saykit`](https://githu
20
20
  import { createCatalogue, createStore } from 'saykit';
21
21
 
22
22
  export const catalogue = createCatalogue({
23
- en: () => import('./locales/en.po'),
24
- fr: () => import('./locales/fr.po'),
23
+ en: () => import('./locales/en.js'),
24
+ fr: () => import('./locales/fr.js'),
25
25
  });
26
26
 
27
27
  const initial = catalogue.match(navigator.languages);
package/dist/client.d.mts CHANGED
@@ -1,19 +1,12 @@
1
1
  import { PropsWithChildren } from "react";
2
2
  import { Store, View } from "saykit";
3
- //#region src/runtime/client.d.ts
4
3
  /**
5
4
  * Where a provider takes its view from.
6
5
  *
7
- * A store is the reactive form: it owns a catalogue, so it can switch locale,
8
- * and every consumer re-renders when it does. Being a live object, it cannot
9
- * cross the server/client boundary.
10
- *
11
- * A locale and its messages are the serialisable form a server can hand across
12
- * that boundary. Only that one locale comes over, so the provider built from
13
- * it has nothing to switch to: switching is the server's to do, normally
14
- * through navigation.
6
+ * A store is reactive and can switch locale, but cannot cross the server/client boundary.
7
+ * A locale and its messages can, though the provider built from them has nothing to switch to.
15
8
  */
16
- type SayProviderProps = {
9
+ export type SayProviderProps = {
17
10
  store: Store;
18
11
  locale?: never;
19
12
  messages?: never;
@@ -42,7 +35,7 @@ type SayProviderProps = {
42
35
  * @param props.options How that locale's view formats its messages, as the
43
36
  * server's view was built with
44
37
  */
45
- declare function SayProvider({ store, locale, messages, options, children }: PropsWithChildren<SayProviderProps>): import("react").FunctionComponentElement<import("react").ProviderProps<any>>;
38
+ export declare function SayProvider({ store, locale, messages, options, children }: PropsWithChildren<SayProviderProps>): import("react").FunctionComponentElement<import("react").ProviderProps<Store<string> | null>>;
46
39
  /**
47
40
  * Get the current {@link View}, on the client.
48
41
  * Must be called within a {@link SayProvider}.
@@ -58,6 +51,4 @@ declare function SayProvider({ store, locale, messages, options, children }: Pro
58
51
  * @returns The current {@link View}
59
52
  * @throws If no provider is in the component tree
60
53
  */
61
- declare function useSay(): View;
62
- //#endregion
63
- export { SayProvider, SayProviderProps, useSay };
54
+ export declare function useSay(): View;
package/dist/client.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { createContext, createElement, useContext, useMemo, useSyncExternalStore } from "react";
3
3
  import { createCatalogue, createStore } from "saykit";
4
- //#region src/runtime/client.ts
4
+ // src/runtime/client.ts
5
5
  const SayContext = createContext(null);
6
6
  SayContext.displayName = "SayContext";
7
7
  function SayProvider({ store, locale, messages, options, children }) {
@@ -21,7 +21,7 @@ function SayProvider({ store, locale, messages, options, children }) {
21
21
  function useSay() {
22
22
  const store = useContext(SayContext);
23
23
  if (!store) throw new Error("'useSay' must be used within a 'SayProvider'");
24
- return useSyncExternalStore(useMemo(() => (listener) => store.subscribe(listener), [store]), () => store.say, () => store.say);
24
+ const subscribe = useMemo(() => (listener) => store.subscribe(listener), [store]);
25
+ return useSyncExternalStore(subscribe, () => store.say, () => store.say);
25
26
  }
26
- //#endregion
27
27
  export { SayProvider, useSay };
@@ -1,18 +1,12 @@
1
1
  import { ReactNode } from "react";
2
2
  import "server-only";
3
- //#region src/runtime/client.server.d.ts
4
3
  /**
5
4
  * The server build of `@saykit/react/client`.
6
5
  *
7
- * A store is a live object and cannot cross the server/client boundary, and a
8
- * server component cannot hand its own scope to a client one either. What can
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.
6
+ * A store cannot cross the server/client boundary, but the locale, messages and options can.
7
+ * This reads them off the view set by {@link import('./server.js').setSay} and passes them
8
+ * to the real provider, so `<SayProvider>` on the server takes no props.
13
9
  */
14
- declare function SayProvider({ children }: {
10
+ export declare function SayProvider({ children }: {
15
11
  children?: ReactNode;
16
- }): import("react").FunctionComponentElement<import("react").PropsWithChildren<import("./client.js").SayProviderProps>>;
17
- //#endregion
18
- export { SayProvider };
12
+ }): import("react").FunctionComponentElement<import("react").PropsWithChildren<import("./client.js").SayProviderProps>>;
@@ -2,7 +2,7 @@ import { createElement } from "react";
2
2
  import "server-only";
3
3
  import { SayProvider as SayProvider$1 } from "./client.mjs";
4
4
  import { getSay } from "./server.mjs";
5
- //#region src/runtime/client.server.ts
5
+ // src/runtime/client.server.ts
6
6
  function SayProvider({ children }) {
7
7
  const say = getSay();
8
8
  return createElement(SayProvider$1, {
@@ -11,5 +11,4 @@ function SayProvider({ children }) {
11
11
  options: say.options
12
12
  }, children);
13
13
  }
14
- //#endregion
15
14
  export { SayProvider };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ReactElement, ReactNode } from "react";
2
2
  import { DateTimeOptions, Disallow, Named, NumberOptions, NumeralOptions, SelectOptions } from "saykit";
3
- //#region src/types.d.ts
4
3
  /**
5
4
  * What a message is allowed to contain. On top of everything React renders, a
6
5
  * named placeholder, `{{ name: value }}`, reaches the type checker as a plain
@@ -13,8 +12,6 @@ type PropsWithSayChildren<P = unknown> = P & {
13
12
  children?: SayNode;
14
13
  };
15
14
  type PropsWithJSXSafeKeys<T> = { [K in keyof T as K extends number | `${number}${string}` ? `_${K}` : K extends `_${number}${string}` ? never : K]: T[K]; };
16
- //#endregion
17
- //#region src/runtime/index.d.ts
18
15
  /**
19
16
  * Render the translation for a descriptor.
20
17
  *
@@ -22,12 +19,12 @@ type PropsWithJSXSafeKeys<T> = { [K in keyof T as K extends number | `${number}$
22
19
  * @returns The translation node for the descriptor
23
20
  * @remark This is a macro and must be used with the relevant saykit plugin
24
21
  */
25
- declare function Say(props: PropsWithSayChildren<{
22
+ export declare function Say(props: PropsWithSayChildren<{
26
23
  id?: string;
27
24
  context?: string;
28
25
  whitespace?: boolean;
29
26
  }>): ReactElement;
30
- declare namespace Say {
27
+ export declare namespace Say {
31
28
  /**
32
29
  * Define a pluralised message.
33
30
  *
@@ -148,5 +145,4 @@ declare namespace Say {
148
145
  _: globalThis.Date | number | Named<globalThis.Date | number>;
149
146
  } & Disallow<DateTimeOptions, 'id' | 'context'>): ReactNode;
150
147
  }
151
- //#endregion
152
- export { Say };
148
+ export {};
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client"; import { useSay as GET_SAY } from "./client.mjs";import { Fragment, cloneElement, createElement, isValidElement } from "react";
2
- //#region src/components/renderer.ts
2
+ // src/components/renderer.ts
3
3
  function Renderer({ html, components, whitespace = true }) {
4
4
  function getComponent(tag) {
5
5
  if (typeof components === "function")
@@ -24,7 +24,8 @@ function Renderer({ html, components, whitespace = true }) {
24
24
  current.push(createElement(Component, { key: nodeKey++ }));
25
25
  } else if (isClosing) {
26
26
  const last = stack.pop();
27
- const element = createElement(getComponent(last.tag), { key: nodeKey++ }, ...current);
27
+ const Component = getComponent(last.tag);
28
+ const element = createElement(Component, { key: nodeKey++ }, ...current);
28
29
  current = last.children;
29
30
  current.push(element);
30
31
  } else {
@@ -45,16 +46,14 @@ function Renderer({ html, components, whitespace = true }) {
45
46
  }
46
47
  return createElement(Fragment, void 0, ...parseNode(html));
47
48
  }
48
- //#endregion
49
- //#region src/types.ts
49
+ // src/types.ts
50
50
  function resolveValuePropKeys(props) {
51
51
  const result = {};
52
52
  for (const key in props) if (key.startsWith("_")) result[key.slice(1)] = props[key];
53
53
  else result[key] = props[key];
54
54
  return result;
55
55
  }
56
- //#endregion
57
- //#region src/runtime/index.ts
56
+ // src/runtime/index.ts
58
57
  function Say(props) {
59
58
  if (!("id" in props) && !("message" in props)) throw new Error("'Say' is a macro and must be used with the relevant saykit plugin", { cause: /* @__PURE__ */ new Error("The 'id' property is required for a descriptor") });
60
59
  const say = GET_SAY();
@@ -106,5 +105,4 @@ function Say(props) {
106
105
  }
107
106
  _Say.Time = Time;
108
107
  })(Say || (Say = {}));
109
- //#endregion
110
108
  export { Say };
@@ -1,5 +1,5 @@
1
1
  import { getSay as GET_SAY } from "./server.mjs";import { Fragment, cloneElement, createElement, isValidElement } from "react";
2
- //#region src/components/renderer.ts
2
+ // src/components/renderer.ts
3
3
  function Renderer({ html, components, whitespace = true }) {
4
4
  function getComponent(tag) {
5
5
  if (typeof components === "function")
@@ -24,7 +24,8 @@ function Renderer({ html, components, whitespace = true }) {
24
24
  current.push(createElement(Component, { key: nodeKey++ }));
25
25
  } else if (isClosing) {
26
26
  const last = stack.pop();
27
- const element = createElement(getComponent(last.tag), { key: nodeKey++ }, ...current);
27
+ const Component = getComponent(last.tag);
28
+ const element = createElement(Component, { key: nodeKey++ }, ...current);
28
29
  current = last.children;
29
30
  current.push(element);
30
31
  } else {
@@ -45,16 +46,14 @@ function Renderer({ html, components, whitespace = true }) {
45
46
  }
46
47
  return createElement(Fragment, void 0, ...parseNode(html));
47
48
  }
48
- //#endregion
49
- //#region src/types.ts
49
+ // src/types.ts
50
50
  function resolveValuePropKeys(props) {
51
51
  const result = {};
52
52
  for (const key in props) if (key.startsWith("_")) result[key.slice(1)] = props[key];
53
53
  else result[key] = props[key];
54
54
  return result;
55
55
  }
56
- //#endregion
57
- //#region src/runtime/index.ts
56
+ // src/runtime/index.ts
58
57
  function Say(props) {
59
58
  if (!("id" in props) && !("message" in props)) throw new Error("'Say' is a macro and must be used with the relevant saykit plugin", { cause: /* @__PURE__ */ new Error("The 'id' property is required for a descriptor") });
60
59
  const say = GET_SAY();
@@ -106,5 +105,4 @@ function Say(props) {
106
105
  }
107
106
  _Say.Time = Time;
108
107
  })(Say || (Say = {}));
109
- //#endregion
110
108
  export { Say };
package/dist/server.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { ReactNode } from "react";
2
2
  import { Catalogue, View } from "saykit";
3
3
  import "server-only";
4
- //#region src/runtime/server.d.ts
5
4
  /**
6
5
  * Get the current {@link View}, on the server.
7
6
  * Must be called below a {@link setSay}, which {@link createWithSay} does for
@@ -20,20 +19,18 @@ import "server-only";
20
19
  * @returns The current {@link View}
21
20
  * @throws If no view has been established for this request
22
21
  */
23
- declare function getSay(): View;
22
+ export declare function getSay(): View;
24
23
  /**
25
24
  * Establish the {@link View} for everything rendered after this point, on the
26
25
  * server. Reach for it when you already have a view; {@link createWithSay}
27
26
  * negotiates and loads one for you.
28
27
  *
29
- * Per request is the limit. React renders a server component's children after
30
- * it returns, so a view does not end where a subtree does: a second view takes
31
- * over for everything rendered after it, including the messages
32
- * `<SayProvider>` serialises. Development warns when that happens.
28
+ * A view lasts the whole request, not a subtree: a second view takes over everything rendered
29
+ * after it. Development warns when that happens.
33
30
  *
34
31
  * @param view The view to establish
35
32
  */
36
- declare function setSay(view: View): void;
33
+ export declare function setSay(view: View): void;
37
34
  /**
38
35
  * Bind a `withSay` to a {@link Catalogue}, normally once beside the catalogue
39
36
  * itself.
@@ -47,9 +44,8 @@ declare function setSay(view: View): void;
47
44
  * layout above it - Next.js does - and a parent that has not run yet has
48
45
  * established nothing.
49
46
  *
50
- * A `<SayProvider>` written inside a wrapped component takes no props of its
51
- * own: the server build of `@saykit/react/client` reads the established view
52
- * and serialises the locale and its messages across the boundary.
47
+ * A `<SayProvider>` inside a wrapped component takes no props: the server build of
48
+ * `@saykit/react/client` reads the established view.
53
49
  *
54
50
  * @example
55
51
  * ```tsx
@@ -63,6 +59,4 @@ declare function setSay(view: View): void;
63
59
  * @param catalogue The catalogue to take views from
64
60
  * @returns A `withSay` bound to that catalogue
65
61
  */
66
- declare function createWithSay<Locale extends string>(catalogue: Catalogue<Locale>): <P>(Component: (props: P) => ReactNode, locale: (props: P) => Catalogue.Guess | Promise<Catalogue.Guess>) => (props: P) => Promise<ReactNode>;
67
- //#endregion
68
- export { createWithSay, getSay, setSay };
62
+ export declare function createWithSay<Locale extends string>(catalogue: Catalogue<Locale>): <P>(Component: (props: P) => ReactNode, locale: (props: P) => Catalogue.Guess | Promise<Catalogue.Guess>) => (props: P) => Promise<ReactNode>;
package/dist/server.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { cache, createElement } from "react";
2
2
  import "server-only";
3
- //#region src/runtime/server.ts
3
+ // src/runtime/server.ts
4
4
  const cell = cache(() => ({
5
5
  view: void 0,
6
6
  warned: false
7
7
  }));
8
8
  const NO_VIEW = "'getSay' must be called below a 'withSay'. Wrap the component in 'withSay(Component, (props) => props.params.then((params) => params.locale))'.";
9
- const SECOND_VIEW = (established, next) => `A view for '${next}' was established while '${established}' was already established for this request. A view is per request rather than per subtree: React renders a server component's children after it returns, so there is nowhere to put the previous view back, and everything rendered after this point reads '${next}' - including components outside the one that established it, and the messages 'SayProvider' serialises to the client. Render the other locale in its own request, or resolve its view yourself and pass it to the components that need it.`;
9
+ const SECOND_VIEW = (established, next) => `A view for '${next}' was established while '${established}' was already established for this request. A view is per request rather than per subtree: React renders a server component's children after it returns, so there is nowhere to put the previous view back, and everything rendered after this point reads '${next}' - including components outside the one that established it, and the messages 'SayProvider' hands to the client. Render the other locale in its own request, or resolve its view yourself and pass it to the components that need it.`;
10
10
  function getSay() {
11
11
  const view = cell().view;
12
12
  if (!view) throw new Error(NO_VIEW);
@@ -28,5 +28,4 @@ function createWithSay(catalogue) {
28
28
  };
29
29
  };
30
30
  }
31
- //#endregion
32
31
  export { createWithSay, getSay, setSay };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/react",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "React integration for saykit, i18n hooks and components",
5
5
  "keywords": [
6
6
  "i18n",
@@ -23,6 +23,9 @@
23
23
  "!dist/**/*.map"
24
24
  ],
25
25
  "type": "module",
26
+ "imports": {
27
+ "#/*": "./src/*"
28
+ },
26
29
  "exports": {
27
30
  ".": {
28
31
  "types": "./dist/index.d.mts",
@@ -47,13 +50,13 @@
47
50
  "server-only": "^0.0.1"
48
51
  },
49
52
  "devDependencies": {
50
- "@testing-library/react": "^16.3.2",
51
- "@types/react": "^19.2.17",
52
- "@types/react-dom": "^19.2.3",
53
- "jsdom": "^29.1.1",
54
- "react": "^19.2.8",
55
- "react-dom": "^19.2.8",
56
- "saykit": "^0.11.1"
53
+ "@testing-library/react": "^16.3.3",
54
+ "@types/react": "^19.3.0",
55
+ "@types/react-dom": "^19.3.0",
56
+ "jsdom": "^30.1.3",
57
+ "react": "^19.3.1",
58
+ "react-dom": "^19.3.1",
59
+ "saykit": "^0.12.0"
57
60
  },
58
61
  "peerDependencies": {
59
62
  "react": "*",