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

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![Coverage](https://codecov.io/gh/k0d13/saykit/graph/badge.svg?flag=integration-react)](https://codecov.io/gh/k0d13/saykit?flags%5B0%5D=integration-react)
6
6
 
7
- A `<Say>` component for rendering translated content in server and client components, a `<SayProvider>` and `useSay()` for client trees, and a `<SayScope>` and `getSay()` that mirror them on the server.
7
+ A `<Say>` component for rendering translated content in server and client components, a `<SayProvider>` and `useSay()` for client trees, and a `withSay` and `getSay()` that mirror them on the server.
8
8
 
9
9
  ## Install
10
10
 
@@ -7,7 +7,7 @@ import "server-only";
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
9
  * cross is the locale and its messages, so this reads them off the view the
10
- * enclosing {@link import('./server.js').SayScope} established and passes them
10
+ * the enclosing segment established with {@link import('./server.js').setSay}, and passes them
11
11
  * to the real provider, which is why `<SayProvider>` written on the server
12
12
  * takes no props.
13
13
  */
package/dist/index.mjs CHANGED
@@ -56,14 +56,17 @@ function resolveValuePropKeys(props) {
56
56
  //#endregion
57
57
  //#region src/runtime/index.ts
58
58
  function Say(props) {
59
- if (!("id" 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") });
59
+ 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
60
  const say = GET_SAY();
61
- const { id, whitespace, ...rest } = props;
61
+ const { id, message, whitespace, ...rest } = props;
62
62
  const values = resolveValuePropKeys(rest);
63
63
  return createElement(Renderer, {
64
- html: say.call({
64
+ html: say.call(message === void 0 ? {
65
65
  ...rest,
66
66
  id
67
+ } : {
68
+ ...rest,
69
+ message
67
70
  }),
68
71
  whitespace,
69
72
  components(tag) {
@@ -56,14 +56,17 @@ function resolveValuePropKeys(props) {
56
56
  //#endregion
57
57
  //#region src/runtime/index.ts
58
58
  function Say(props) {
59
- if (!("id" 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") });
59
+ 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
60
  const say = GET_SAY();
61
- const { id, whitespace, ...rest } = props;
61
+ const { id, message, whitespace, ...rest } = props;
62
62
  const values = resolveValuePropKeys(rest);
63
63
  return createElement(Renderer, {
64
- html: say.call({
64
+ html: say.call(message === void 0 ? {
65
65
  ...rest,
66
66
  id
67
+ } : {
68
+ ...rest,
69
+ message
67
70
  }),
68
71
  whitespace,
69
72
  components(tag) {
package/dist/server.d.mts CHANGED
@@ -4,7 +4,8 @@ import "server-only";
4
4
  //#region src/runtime/server.d.ts
5
5
  /**
6
6
  * Get the current {@link View}, on the server.
7
- * Must be called below a {@link SayScope}.
7
+ * Must be called below a {@link setSay}, which {@link createWithSay} does for
8
+ * you.
8
9
  *
9
10
  * The server counterpart of `useSay`. Reach for it when you need the locale as
10
11
  * *data*, to build an `Intl.NumberFormat` say, rather than as a rendered
@@ -17,61 +18,51 @@ import "server-only";
17
18
  * ```
18
19
  *
19
20
  * @returns The current {@link View}
20
- * @throws If no {@link SayScope} is above the caller
21
+ * @throws If no view has been established for this request
21
22
  */
22
23
  declare function getSay(): View;
23
- declare namespace SayScope {
24
- /**
25
- * Which view a scope establishes: a catalogue and a locale to negotiate
26
- * against it, or a view already resolved.
27
- */
28
- type Props<Locale extends string = string> = {
29
- children?: ReactNode;
30
- } & ({
31
- catalogue: Catalogue<Locale>;
32
- locale: Catalogue.Guess;
33
- view?: never;
34
- } | {
35
- view: View<Locale>;
36
- catalogue?: never;
37
- locale?: never;
38
- });
39
- }
40
24
  /**
41
- * Establish the {@link View} for everything rendered inside it, on the server.
25
+ * Establish the {@link View} for everything rendered after this point, on the
26
+ * server. Reach for it when you already have a view; {@link createWithSay}
27
+ * negotiates and loads one for you.
42
28
  *
43
- * Given a catalogue and a locale, the locale is negotiated against the
44
- * catalogue and its messages are loaded before the children render. Given a
45
- * view, that view is established as it is. Either way `<Say>` and
46
- * {@link getSay} resolve at any depth below, and the scope is per request.
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.
47
33
  *
48
- * Per request is also the limit. React renders a server component's children
49
- * after it returns, so a scope does not end where its children do: a second
50
- * scope takes over for everything rendered after it, including components
51
- * outside it and the messages `<SayProvider>` serialises. Development warns
52
- * when that happens. Render another locale in its own request, or resolve its
53
- * view yourself and pass it to the components that need it.
34
+ * @param view The view to establish
35
+ */
36
+ declare function setSay(view: View): void;
37
+ /**
38
+ * Bind a `withSay` to a {@link Catalogue}, normally once beside the catalogue
39
+ * itself.
54
40
  *
55
- * A `<SayProvider>` written inside one takes no props of its own: the server
56
- * build of `@saykit/react/client` reads the established view and serialises
57
- * the locale and its messages across the boundary.
41
+ * `withSay` wraps a server component so its view is negotiated, loaded and
42
+ * established before the component renders, which `<Say>` and {@link getSay}
43
+ * then read at any depth below.
58
44
  *
59
- * @example
60
- * ```tsx
61
- * <SayScope catalogue={catalogue} locale={locale}>
62
- * <SayProvider>{children}</SayProvider>
63
- * </SayScope>
64
- * ```
45
+ * Every route segment that renders messages wraps itself, rather than
46
+ * inheriting from a parent. A framework is free to render a page before the
47
+ * layout above it - Next.js does - and a parent that has not run yet has
48
+ * established nothing.
49
+ *
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.
65
53
  *
66
54
  * @example
67
55
  * ```tsx
68
- * <SayScope view={await catalogue.load('fr')}>{children}</SayScope>
56
+ * // i18n.ts
57
+ * export const withSay = createWithSay(catalogue);
58
+ *
59
+ * // app/[locale]/page.tsx
60
+ * export default withSay(Page, (props) => props.params.then((params) => params.locale));
69
61
  * ```
70
62
  *
71
- * @param props.catalogue The catalogue to take the view from
72
- * @param props.locale The locale to negotiate against it
73
- * @param props.view A view to establish as it is, instead of both of those
63
+ * @param catalogue The catalogue to take views from
64
+ * @returns A `withSay` bound to that catalogue
74
65
  */
75
- declare function SayScope<Locale extends string>({ catalogue, locale, view, children }: SayScope.Props<Locale>): Promise<ReactNode>;
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>;
76
67
  //#endregion
77
- export { SayScope, getSay };
68
+ export { createWithSay, getSay, setSay };
package/dist/server.mjs CHANGED
@@ -1,26 +1,32 @@
1
- import { cache } from "react";
1
+ import { cache, createElement } from "react";
2
2
  import "server-only";
3
3
  //#region src/runtime/server.ts
4
4
  const cell = cache(() => ({
5
5
  view: void 0,
6
6
  warned: false
7
7
  }));
8
- const NO_VIEW = "'getSay' must be called below a 'SayScope'. Wrap the tree in '<SayScope catalogue={catalogue} locale={locale}>'.";
9
- const NESTED_SCOPE = (established, next) => `A 'SayScope' established '${next}' while '${established}' was already established for this request. A scope 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 inner scope, 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.`;
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.`;
10
10
  function getSay() {
11
11
  const view = cell().view;
12
12
  if (!view) throw new Error(NO_VIEW);
13
13
  return view;
14
14
  }
15
- async function SayScope({ catalogue, locale, view, children }) {
16
- const resolved = view ?? await catalogue.load(catalogue.match(locale));
15
+ function setSay(view) {
17
16
  const request = cell();
18
- if (process.env.NODE_ENV !== "production" && !request.warned && request.view && request.view.locale !== resolved.locale) {
17
+ if (process.env.NODE_ENV !== "production" && !request.warned && request.view && request.view.locale !== view.locale) {
19
18
  request.warned = true;
20
- console.warn(NESTED_SCOPE(request.view.locale, resolved.locale));
19
+ console.warn(SECOND_VIEW(request.view.locale, view.locale));
21
20
  }
22
- request.view = resolved;
23
- return children;
21
+ request.view = view;
22
+ }
23
+ function createWithSay(catalogue) {
24
+ return function withSay(Component, locale) {
25
+ return async function WithSay(props) {
26
+ setSay(await catalogue.load(catalogue.match(await locale(props))));
27
+ return createElement(Component, props);
28
+ };
29
+ };
24
30
  }
25
31
  //#endregion
26
- export { SayScope, getSay };
32
+ export { createWithSay, getSay, setSay };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saykit/react",
3
- "version": "0.0.0-beta-20260903075415",
3
+ "version": "0.0.0-beta-20260911005306",
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-20260903075415"
56
+ "saykit": "^0.0.0-beta-20260911005306"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": "*",
60
- "saykit": "0.0.0-beta-20260903075415"
60
+ "saykit": "0.0.0-beta-20260911005306"
61
61
  },
62
62
  "scripts": {
63
63
  "check": "tsc --noEmit",