;
+}) {
+ const executor = useExecutor(id, variables);
+ return (
+ {typeof executor === 'function' ? executor(variables) : executor}
+ );
+}
+
+test('no operator', () => {
+ expect(() => render()).toThrow();
+});
+
+test('global default operator', () => {
+ expect(() =>
+ render(
+ 'default'}>
+
+ ,
+ ),
+ ).not.toThrow();
+ expect(screen.getByText('default')).toBeDefined();
+});
+
+test('global default operator with arguments', () => {
+ expect(() =>
+ render(
+ vars.foo}>
+
+ ,
+ ),
+ ).not.toThrow();
+ expect(screen.getByText('bar')).toBeDefined();
+});
+
+test('operation default operator', () => {
+ expect(() =>
+ render(
+ 'operation a'}>
+
+ ,
+ ),
+ ).not.toThrow();
+
+ expect(screen.getByText('operation a')).toBeDefined();
+});
+
+test('operation default operator with arguments', () => {
+ expect(() =>
+ render(
+ vars.foo}>
+
+ ,
+ ),
+ ).not.toThrow();
+ expect(screen.getByText('bar')).toBeDefined();
+});
+
+test('structural argument matching', () => {
+ const id = 'x';
+ const a = vi.fn();
+ const b = vi.fn();
+ const c = vi.fn();
+
+ expect(() =>
+ render(
+
+
+
+
+
+
+
+
+ ,
+ ),
+ ).not.toThrow();
+
+ expect(a).toHaveBeenCalledOnce();
+ expect(a).toHaveBeenCalledWith(id, { y: 1 });
+ expect(b).toHaveBeenCalledOnce();
+ expect(b).toHaveBeenCalledWith(id, { y: 2 });
+ expect(c).toHaveBeenCalledOnce();
+ expect(c).toHaveBeenCalledWith(id, { y: 1, z: 1 });
+});
+
+test('structural argument mismatch', () => {
+ const id = 'x';
+ const a = vi.fn();
+ const b = vi.fn();
+ const c = vi.fn();
+
+ expect(() =>
+ render(
+
+
+
+
+
+
+ ,
+ ),
+ ).toThrow(
+ 'No executor found for: x:{"y":3} Candidates: x:{"y":1} x:{"y":2} x:{"y":1,"z":1}',
+ );
+});
+
+test('static data resolution', () => {
+ const id = 'x';
+
+ expect(() =>
+ render(
+ 'static data'}>
+
+ ,
+ ),
+ ).not.toThrow();
+ expect(screen.getByText('static data')).toBeDefined();
+});
+
+test('static data resolution with arguments', () => {
+ const id = 'x';
+
+ expect(() =>
+ render(
+
+
+ ,
+ ),
+ ).not.toThrow();
+ expect(screen.getByText('static data')).toBeDefined();
+});
+
+test('static data updates', () => {
+ const id = 'x';
+
+ function Executor({ children }: PropsWithChildren) {
+ const [count, setCount] = useState(0);
+ return (
+ <>
+
+
+ {children}
+
+ >
+ );
+ }
+
+ expect(() =>
+ render(
+
+
+ ,
+ ),
+ ).not.toThrow();
+
+ expect(screen.getByText(0)).toBeDefined();
+
+ act(() => {
+ fireEvent.click(screen.getByText(/Up/));
+ });
+
+ expect(screen.getByText(1)).toBeDefined();
+});
diff --git a/packages/executors/src/lib.tsx b/packages/executors/src/lib.tsx
new file mode 100644
index 000000000..3f2844e56
--- /dev/null
+++ b/packages/executors/src/lib.tsx
@@ -0,0 +1,72 @@
+import { isMatch } from 'lodash-es';
+
+import { RegistryEntry } from '../types';
+
+type VariablesMatcher =
+ | Record
+ | ((vars: Record) => boolean);
+
+function executorMap(executors: RegistryEntry[]) {
+ return Object.fromEntries(
+ executors.map((ex) => [`${ex.id}:${JSON.stringify(ex.variables)}`, ex]),
+ );
+}
+
+export function mergeExecutors(
+ oldExecutors: RegistryEntry[],
+ newExecutors: RegistryEntry[],
+): RegistryEntry[] {
+ return Object.values(
+ Object.assign({}, executorMap(oldExecutors), executorMap(newExecutors)),
+ );
+}
+
+export function matchVariables(
+ matcher: VariablesMatcher | undefined,
+ variables: any,
+) {
+ if (typeof matcher === 'undefined') {
+ return true;
+ }
+ if (typeof matcher === 'function') {
+ return matcher(variables);
+ }
+ return isMatch(variables, matcher);
+}
+
+export function getCandidates(id: string, registry: RegistryEntry[]) {
+ return (registry as Array).filter(
+ (entry) => id === entry.id || entry.id === undefined,
+ );
+}
+
+function formatEntry(id: string | undefined, variables?: Record) {
+ return `${id ? id : '*'}:${variables ? JSON.stringify(variables) : '*'}`;
+}
+
+export class ExecutorRegistryError extends Error {
+ constructor(
+ registry: RegistryEntry[],
+ id: string,
+ variables?: Record,
+ ) {
+ const candidates = getCandidates(id, registry);
+ const candidatesMessage =
+ candidates.length > 0
+ ? [
+ 'Candidates:',
+ ...candidates.map(({ id, variables }) =>
+ formatEntry(id, variables),
+ ),
+ ]
+ : [];
+ super(
+ [
+ 'No executor found for:',
+ formatEntry(id, variables),
+ ...candidatesMessage,
+ ].join(' '),
+ );
+ this.name = 'ExecutorRegistryError';
+ }
+}
diff --git a/packages/executors/src/server.tsx b/packages/executors/src/server.tsx
new file mode 100644
index 000000000..c2c2797a4
--- /dev/null
+++ b/packages/executors/src/server.tsx
@@ -0,0 +1,44 @@
+import { PropsWithChildren } from 'react';
+
+import type {
+ ExecuteOperation as ExecuteOperationType,
+ RegistryEntry,
+} from '../types';
+import {
+ ExecutorRegistryError,
+ getCandidates,
+ matchVariables,
+ mergeExecutors,
+} from './lib';
+
+let registry: RegistryEntry[] = [];
+
+export function useExecutor(id: string, variables?: Record) {
+ const op = getCandidates(id, registry)
+ .filter((entry) => matchVariables(entry.variables, variables))
+ .pop();
+ if (op) {
+ if (typeof op.executor === 'function') {
+ return (vars?: Record) => op.executor(id, vars);
+ }
+ return op.executor;
+ }
+ throw new ExecutorRegistryError(registry, id, variables);
+}
+
+export function OperationExecutor({
+ children,
+ ...entry
+}: PropsWithChildren) {
+ registry = mergeExecutors(registry, [entry]);
+ return children;
+}
+
+export const ExecuteOperation: typeof ExecuteOperationType = async ({
+ id,
+ variables,
+ children,
+}) => {
+ const result = await useExecutor(id, variables)(variables);
+ return children({ loading: false, result });
+};
diff --git a/packages/executors/tsconfig.json b/packages/executors/tsconfig.json
new file mode 100644
index 000000000..e7c17f5d2
--- /dev/null
+++ b/packages/executors/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "declaration": true,
+ "declarationDir": "build/dts",
+ "outDir": "build",
+ "jsx": "react-jsx",
+ },
+ "include": ["src"]
+}
diff --git a/packages/executors/turbo.json b/packages/executors/turbo.json
new file mode 100644
index 000000000..66be584b4
--- /dev/null
+++ b/packages/executors/turbo.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://turborepo.org/schema.json",
+ "extends": ["//"],
+ "pipeline": {
+ "prep": {
+ "outputs": ["build"]
+ }
+ }
+}
diff --git a/packages/executors/types.d.ts b/packages/executors/types.d.ts
new file mode 100644
index 000000000..ffb892962
--- /dev/null
+++ b/packages/executors/types.d.ts
@@ -0,0 +1,30 @@
+import { JSX, PropsWithChildren } from 'react';
+
+type Executor =
+ | any
+ | ((id: string, variables: Record) => any | Promise);
+
+type VariablesMatcher =
+ | Record
+ | ((vars: Record) => boolean);
+
+export type RegistryEntry = {
+ executor: Executor;
+ id?: string;
+ variables?: VariablesMatcher;
+};
+
+export function OperationExecutor(
+ props: PropsWithChildren,
+): JSX.Element;
+
+export function useExecutor(
+ id: string,
+ variables?: Record,
+): (vars?: Record) => any | Promise;
+
+export function ExecuteOperation(props: {
+ id: string;
+ variables?: Record;
+ children: (result: { loading: boolean; result: any }) => JSX.Element;
+}): JSX.Element | Promise;
diff --git a/packages/react-intl/client.js b/packages/react-intl/client.js
new file mode 100644
index 000000000..4d2021998
--- /dev/null
+++ b/packages/react-intl/client.js
@@ -0,0 +1,2 @@
+'use client';
+export { IntlProvider, useIntl } from 'react-intl';
diff --git a/packages/react-intl/index.d.ts b/packages/react-intl/index.d.ts
new file mode 100644
index 000000000..999586887
--- /dev/null
+++ b/packages/react-intl/index.d.ts
@@ -0,0 +1 @@
+export { IntlProvider, useIntl } from 'react-intl';
diff --git a/packages/react-intl/package.json b/packages/react-intl/package.json
new file mode 100644
index 000000000..5e9f6209b
--- /dev/null
+++ b/packages/react-intl/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@amazeelabs/react-intl",
+ "version": "1.0.0",
+ "description": "Minimal RSC-compatible wrapper around react-intl",
+ "main": "client.js",
+ "types": "index.d.ts",
+ "private": false,
+ "sideEffects": false,
+ "type": "module",
+ "keywords": [],
+ "author": "Amazee Labs ",
+ "license": "ISC",
+ "dependencies": {
+ "react-intl": "^6.6.5",
+ "react": "19.0.0-rc.0",
+ "react-server-dom-webpack": "19.0.0-rc.0"
+ },
+ "exports": {
+ ".": {
+ "react-server": "./server.js",
+ "default": "./client.js"
+ }
+ },
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+}
diff --git a/packages/react-intl/server.js b/packages/react-intl/server.js
new file mode 100644
index 000000000..50c6eecbd
--- /dev/null
+++ b/packages/react-intl/server.js
@@ -0,0 +1,15 @@
+import { createElement } from 'react';
+import { createIntl } from 'react-intl/src/components/createIntl';
+
+import { IntlProvider as ClientIntlProvider } from './client.js';
+
+let intl = null;
+
+export function IntlProvider(props) {
+ intl = createIntl(props);
+ return createElement(ClientIntlProvider, props);
+}
+
+export function useIntl() {
+ return intl;
+}
diff --git a/packages/schema/package.json b/packages/schema/package.json
index bcbdd2da1..a1b874b17 100644
--- a/packages/schema/package.json
+++ b/packages/schema/package.json
@@ -62,7 +62,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
- "@amazeelabs/executors": "^2.0.2",
+ "@amazeelabs/executors": "workspace:*",
"@amazeelabs/gatsby-silverback-cloudinary": "^1.2.7",
"@amazeelabs/gatsby-source-silverback": "^1.14.0",
"@amazeelabs/scalars": "^1.6.13",
diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts
index 42559e8d3..aa1975531 100644
--- a/packages/schema/src/index.ts
+++ b/packages/schema/src/index.ts
@@ -1,4 +1,5 @@
import {
+ ExecuteOperation as UntypedExecuteOperation,
OperationExecutor as UntypedOperationExecutor,
useExecutor as untypedUseExecutor,
} from '@amazeelabs/executors';
@@ -32,7 +33,7 @@ export function OperationExecutor(
variables?: VariablesMatcher>;
executor: Executor;
}>,
-) {
+): JSX.Element {
return UntypedOperationExecutor(props);
}
@@ -46,3 +47,18 @@ export function useExecutor(
) => Promise>) {
return untypedUseExecutor(id, variables);
}
+
+export function ExecuteOperation(props: {
+ id: OperationId;
+ variables?: OperationVariables;
+ children: (result: {
+ loading: boolean;
+ result: OperationResult;
+ }) => JSX.Element;
+}):
+ | OperationResult
+ | ((
+ variables?: OperationVariables,
+ ) => Promise>) {
+ return UntypedExecuteOperation(props);
+}
diff --git a/packages/schema/src/operations/ListPages.gql b/packages/schema/src/operations/ListPages.gql
index 4e754e319..d4024d185 100644
--- a/packages/schema/src/operations/ListPages.gql
+++ b/packages/schema/src/operations/ListPages.gql
@@ -1,5 +1,5 @@
-query ListPages {
- ssgPages {
+query ListPages($args: String!) {
+ ssgPages(args: $args) {
rows {
translations {
path
@@ -7,5 +7,4 @@ query ListPages {
}
total
}
-
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 1dc3c6245..c9bd50927 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -49,11 +49,12 @@
"hast-util-select": "^5.0.5",
"query-string": "^9.0.0",
"react-hook-form": "^7.49.2",
- "react-intl": "^6.6.2",
+ "@amazeelabs/react-intl": "workspace:*",
"swr": "^2.2.4",
"unified": "^10.1.2",
"zod": "^3.22.4",
- "zustand": "^4.4.7"
+ "zustand": "^4.4.7",
+ "react-server-dom-webpack": "19.0.0-rc.0"
},
"peerDependencies": {
"react": "^18.2.0",
diff --git a/packages/ui/src/components/Client/MobileMenu.tsx b/packages/ui/src/components/Client/MobileMenu.tsx
index 022402639..4a3bf3abb 100644
--- a/packages/ui/src/components/Client/MobileMenu.tsx
+++ b/packages/ui/src/components/Client/MobileMenu.tsx
@@ -1,4 +1,5 @@
'use client';
+import { useIntl } from '@amazeelabs/react-intl';
import {
Dialog,
DialogPanel,
@@ -9,7 +10,6 @@ import {
import { ChevronDownIcon } from '@heroicons/react/20/solid';
import clsx from 'clsx';
import React, { createContext, PropsWithChildren } from 'react';
-import { useIntl } from 'react-intl';
const MobileMenuContext = createContext({
isOpen: false,
diff --git a/packages/ui/src/components/Molecules/InquiryForm.tsx b/packages/ui/src/components/Molecules/InquiryForm.tsx
index c58db3309..1ae6c5d39 100644
--- a/packages/ui/src/components/Molecules/InquiryForm.tsx
+++ b/packages/ui/src/components/Molecules/InquiryForm.tsx
@@ -1,7 +1,7 @@
+import { useIntl } from '@amazeelabs/react-intl';
import { CreateSubmissionMutation } from '@custom/schema';
import React from 'react';
import { useForm } from 'react-hook-form';
-import { useIntl } from 'react-intl';
import { z } from 'zod';
import { useMutation } from '../../utils/operation';
diff --git a/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx b/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx
index e31a6bc01..938794a28 100644
--- a/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx
+++ b/packages/ui/src/components/Molecules/LanguageSwitcher.stories.tsx
@@ -2,7 +2,10 @@ import { FrameQuery, OperationExecutor, Url } from '@custom/schema';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import React from 'react';
-import { Translations, TranslationsProvider } from '../../utils/translations';
+import {
+ TranslationPaths,
+ TranslationsProvider,
+} from '../../utils/translations';
import { Default } from '../Routes/Frame.stories';
import { LanguageSwitcher } from './LanguageSwitcher';
@@ -27,7 +30,7 @@ const TranslationsDecorator = ((Story, ctx) => {
);
-}) as Decorator;
+}) as Decorator;
export default {
component: LanguageSwitcher,
@@ -35,9 +38,9 @@ export default {
parameters: {
location: new URL('local:/en/english-version'),
},
-} satisfies Meta;
+} satisfies Meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const Empty = {} satisfies Story;
diff --git a/packages/ui/src/components/Molecules/PageTransition.tsx b/packages/ui/src/components/Molecules/PageTransition.tsx
index 72ca7569b..180103c7a 100644
--- a/packages/ui/src/components/Molecules/PageTransition.tsx
+++ b/packages/ui/src/components/Molecules/PageTransition.tsx
@@ -1,8 +1,23 @@
-import { motion, useReducedMotion } from 'framer-motion';
+'use client';
+import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
import React, { PropsWithChildren, useEffect } from 'react';
import { Messages, readMessages } from './Messages';
+export function PageTransitionWrapper({ children }: PropsWithChildren) {
+ return (
+
+ {useReducedMotion() ? (
+ <>{children}>
+ ) : (
+
+ {children}
+
+ )}
+
+ );
+}
+
export function PageTransition({ children }: PropsWithChildren) {
const [messages, setMessages] = React.useState>([]);
useEffect(() => {
diff --git a/packages/ui/src/components/Molecules/Pagination.tsx b/packages/ui/src/components/Molecules/Pagination.tsx
index d85beb270..f26b76880 100644
--- a/packages/ui/src/components/Molecules/Pagination.tsx
+++ b/packages/ui/src/components/Molecules/Pagination.tsx
@@ -1,3 +1,4 @@
+import { useIntl } from '@amazeelabs/react-intl';
import { Link, useLocation } from '@custom/schema';
import {
ArrowLongLeftIcon,
@@ -5,7 +6,6 @@ import {
} from '@heroicons/react/20/solid';
import clsx from 'clsx';
import React from 'react';
-import { useIntl } from 'react-intl';
import { z } from 'zod';
export const paginationParamsSchema = z.object({
diff --git a/packages/ui/src/components/Molecules/SearchForm.tsx b/packages/ui/src/components/Molecules/SearchForm.tsx
index e043a6fcd..989a49fe3 100644
--- a/packages/ui/src/components/Molecules/SearchForm.tsx
+++ b/packages/ui/src/components/Molecules/SearchForm.tsx
@@ -1,8 +1,8 @@
+import { useIntl } from '@amazeelabs/react-intl';
import { useLocation } from '@custom/schema';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { useForm } from 'react-hook-form';
-import { useIntl } from 'react-intl';
import { z } from 'zod';
const formValueSchema = z.object({
diff --git a/packages/ui/src/components/Organisms/ContentHub.tsx b/packages/ui/src/components/Organisms/ContentHub.tsx
index c6ccd3475..5dface3d5 100644
--- a/packages/ui/src/components/Organisms/ContentHub.tsx
+++ b/packages/ui/src/components/Organisms/ContentHub.tsx
@@ -1,7 +1,7 @@
+import { useIntl } from '@amazeelabs/react-intl';
import { ContentHubQuery, Image, Link, Locale } from '@custom/schema';
import qs from 'query-string';
import React from 'react';
-import { useIntl } from 'react-intl';
import { isTruthy } from '../../utils/isTruthy';
import { useOperation } from '../../utils/operation';
diff --git a/packages/ui/src/components/Organisms/Footer.tsx b/packages/ui/src/components/Organisms/Footer.tsx
index 73dc34bdf..e66a600da 100644
--- a/packages/ui/src/components/Organisms/Footer.tsx
+++ b/packages/ui/src/components/Organisms/Footer.tsx
@@ -1,6 +1,7 @@
+'use client';
+import { useIntl } from '@amazeelabs/react-intl';
import { FrameQuery, Link } from '@custom/schema';
import React from 'react';
-import { useIntl } from 'react-intl';
import { isTruthy } from '../../utils/isTruthy';
import { buildNavigationTree } from '../../utils/navigation';
diff --git a/packages/ui/src/components/Organisms/Header.tsx b/packages/ui/src/components/Organisms/Header.tsx
index b132e4ceb..d9ef03f49 100644
--- a/packages/ui/src/components/Organisms/Header.tsx
+++ b/packages/ui/src/components/Organisms/Header.tsx
@@ -1,7 +1,8 @@
+'use client';
+import { useIntl } from '@amazeelabs/react-intl';
import { FrameQuery, Link, Url } from '@custom/schema';
import clsx from 'clsx';
import React from 'react';
-import { useIntl } from 'react-intl';
import { isTruthy } from '../../utils/isTruthy';
import { buildNavigationTree } from '../../utils/navigation';
diff --git a/packages/ui/src/components/Routes/ContentHub.tsx b/packages/ui/src/components/Routes/ContentHub.tsx
index c9b001b80..107406e33 100644
--- a/packages/ui/src/components/Routes/ContentHub.tsx
+++ b/packages/ui/src/components/Routes/ContentHub.tsx
@@ -1,20 +1,23 @@
import { Locale } from '@custom/schema';
import React from 'react';
-import { useTranslations } from '../../utils/translations';
+import { Translations } from '../../utils/translations';
import { PageTransition } from '../Molecules/PageTransition';
import { ContentHub as ContentHubOrganism } from '../Organisms/ContentHub';
export function ContentHub(props: { pageSize: number }) {
- // Initialize the content hub in each language.
- useTranslations(
- Object.fromEntries(
- Object.values(Locale).map((locale) => [locale, `/${locale}/content-hub`]),
- ),
- );
return (
-
+ [
+ locale,
+ `/${locale}/content-hub`,
+ ]),
+ )}
+ >
+
+
);
}
diff --git a/packages/ui/src/components/Routes/Frame.tsx b/packages/ui/src/components/Routes/Frame.tsx
index 6efe5c1c9..67e19a404 100644
--- a/packages/ui/src/components/Routes/Frame.tsx
+++ b/packages/ui/src/components/Routes/Frame.tsx
@@ -1,11 +1,16 @@
-import { FrameQuery, Locale, useLocale } from '@custom/schema';
-import { AnimatePresence, useReducedMotion } from 'framer-motion';
+import { IntlProvider } from '@amazeelabs/react-intl';
+import {
+ ExecuteOperation,
+ FrameQuery,
+ Locale,
+ useLocale,
+ useLocation,
+} from '@custom/schema';
import React, { PropsWithChildren } from 'react';
-import { IntlProvider } from 'react-intl';
import translationSources from '../../../build/translatables.json';
-import { useOperation } from '../../utils/operation';
import { TranslationsProvider } from '../../utils/translations';
+import { PageTransitionWrapper } from '../Molecules/PageTransition';
import { Footer } from '../Organisms/Footer';
import { Header } from '../Organisms/Header';
@@ -30,44 +35,41 @@ function translationsMap(translatables: FrameQuery['stringTranslations']) {
);
}
-function useTranslations() {
- const locale = useLocale();
- const translations = useOperation(FrameQuery).data?.stringTranslations;
- return {
- ...translationsMap(translations?.filter(filterByLocale('en')) || []),
- ...translationsMap(translations?.filter(filterByLocale(locale)) || []),
- };
-}
-
export function Frame(props: PropsWithChildren<{}>) {
const locale = useLocale();
- const translations = useTranslations();
- const messages = Object.fromEntries(
- Object.keys(translationSources).map((key) => [
- key,
- translations[
- translationSources[key as keyof typeof translationSources]
- .defaultMessage
- ] ||
- translationSources[key as keyof typeof translationSources]
- .defaultMessage,
- ]),
- );
return (
-
-
-
-
- {useReducedMotion() ? (
- <>{props.children}>
- ) : (
-
- {props.children}
-
- )}
-
-
-
-
+
+ {({ result }) => {
+ const rawTranslations = result.stringTranslations || [];
+ const translations = {
+ ...translationsMap(
+ rawTranslations?.filter(filterByLocale('en')) || [],
+ ),
+ ...translationsMap(
+ rawTranslations?.filter(filterByLocale(locale)) || [],
+ ),
+ };
+ const messages = Object.fromEntries(
+ Object.keys(translationSources).map((key) => [
+ key,
+ translations[
+ translationSources[key as keyof typeof translationSources]
+ .defaultMessage
+ ] ||
+ translationSources[key as keyof typeof translationSources]
+ .defaultMessage,
+ ]),
+ );
+ return (
+
+
+
+ {props.children}
+
+
+
+ );
+ }}
+
);
}
diff --git a/packages/ui/src/components/Routes/HomePage.tsx b/packages/ui/src/components/Routes/HomePage.tsx
index 2292c204f..46f2627b0 100644
--- a/packages/ui/src/components/Routes/HomePage.tsx
+++ b/packages/ui/src/components/Routes/HomePage.tsx
@@ -1,22 +1,24 @@
-import { HomePageQuery, useLocalized } from '@custom/schema';
+'use client';
+import { HomePageQuery, useLocalized, useLocation } from '@custom/schema';
import React from 'react';
import { isTruthy } from '../../utils/isTruthy';
import { useOperation } from '../../utils/operation';
-import { useTranslations } from '../../utils/translations';
+import { Translations } from '../../utils/translations';
import { PageDisplay } from '../Organisms/PageDisplay';
export function HomePage() {
const { data } = useOperation(HomePageQuery);
const page = useLocalized(data?.websiteSettings?.homePage?.translations);
-
- // Initialize the language switcher with the options this page has.
- useTranslations(
- Object.fromEntries(
- data?.websiteSettings?.homePage?.translations
- ?.filter(isTruthy)
- .map((translation) => [translation.locale, translation.path]) || [],
- ),
+ const translations = Object.fromEntries(
+ data?.websiteSettings?.homePage?.translations
+ ?.filter(isTruthy)
+ .map((translation) => [translation.locale, translation.path]) || [],
);
- return page ? : null;
+
+ return page ? (
+
+
+
+ ) : null;
}
diff --git a/packages/ui/src/components/Routes/Inquiry.tsx b/packages/ui/src/components/Routes/Inquiry.tsx
index 5476fc91d..847b84d37 100644
--- a/packages/ui/src/components/Routes/Inquiry.tsx
+++ b/packages/ui/src/components/Routes/Inquiry.tsx
@@ -1,20 +1,19 @@
import { Locale } from '@custom/schema';
import React from 'react';
-import { useTranslations } from '../../utils/translations';
+import { Translations } from '../../utils/translations';
import { PageTransition } from '../Molecules/PageTransition';
import { Inquiry as InquiryOrganism } from '../Organisms/Inquiry';
export function Inquiry() {
- // Initialize the inquiry page in each language.
- useTranslations(
- Object.fromEntries(
- Object.values(Locale).map((locale) => [locale, `/${locale}/inquiry`]),
- ),
+ const translations = Object.fromEntries(
+ Object.values(Locale).map((locale) => [locale, `/${locale}/inquiry`]),
);
return (
-
+
+
+
);
}
diff --git a/packages/ui/src/components/Routes/Menu.tsx b/packages/ui/src/components/Routes/Menu.tsx
index 17c733fc2..b34257d5d 100644
--- a/packages/ui/src/components/Routes/Menu.tsx
+++ b/packages/ui/src/components/Routes/Menu.tsx
@@ -1,5 +1,5 @@
+import { useIntl } from '@amazeelabs/react-intl';
import { FrameQuery, NavigationItem, Url, useLocation } from '@custom/schema';
-import { useIntl } from 'react-intl';
import { useOperation } from '../../utils/operation';
diff --git a/packages/ui/src/components/Routes/NotFoundPage.tsx b/packages/ui/src/components/Routes/NotFoundPage.tsx
index 749ff11be..a95dd4df4 100644
--- a/packages/ui/src/components/Routes/NotFoundPage.tsx
+++ b/packages/ui/src/components/Routes/NotFoundPage.tsx
@@ -1,3 +1,4 @@
+'use client';
import { NotFoundPageQuery, useLocalized } from '@custom/schema';
import React from 'react';
@@ -7,5 +8,5 @@ import { PageDisplay } from '../Organisms/PageDisplay';
export function NotFoundPage() {
const { data } = useOperation(NotFoundPageQuery);
const page = useLocalized(data?.websiteSettings?.notFoundPage?.translations);
- return page ? : null;
+ return page ? : ;
}
diff --git a/packages/ui/src/components/Routes/Page.tsx b/packages/ui/src/components/Routes/Page.tsx
index 9eb6f24c4..318ce7d9b 100644
--- a/packages/ui/src/components/Routes/Page.tsx
+++ b/packages/ui/src/components/Routes/Page.tsx
@@ -1,9 +1,10 @@
+'use client';
import { useLocation, ViewPageQuery } from '@custom/schema';
import React from 'react';
import { isTruthy } from '../../utils/isTruthy';
import { useOperation } from '../../utils/operation';
-import { useTranslations } from '../../utils/translations';
+import { Translations } from '../../utils/translations';
import { PageDisplay } from '../Organisms/PageDisplay';
export function Page() {
@@ -13,12 +14,14 @@ export function Page() {
const { data } = useOperation(ViewPageQuery, { pathname: loc.pathname });
// Initialize the language switcher with the options this page has.
- useTranslations(
- Object.fromEntries(
- data?.page?.translations
- ?.filter(isTruthy)
- .map((translation) => [translation.locale, translation.path]) || [],
- ),
+ const translations = Object.fromEntries(
+ data?.page?.translations
+ ?.filter(isTruthy)
+ .map((translation) => [translation.locale, translation.path]) || [],
);
- return data?.page ? : null;
+ return data?.page ? (
+
+
+
+ ) : null;
}
diff --git a/packages/ui/src/utils/operation.ts b/packages/ui/src/utils/operation.ts
index f05499b3f..e957a41d9 100644
--- a/packages/ui/src/utils/operation.ts
+++ b/packages/ui/src/utils/operation.ts
@@ -1,3 +1,4 @@
+'use client';
import {
AnyOperationId,
OperationResult,
diff --git a/packages/ui/src/utils/translations.tsx b/packages/ui/src/utils/translations.tsx
index 32e861be2..551ace5fb 100644
--- a/packages/ui/src/utils/translations.tsx
+++ b/packages/ui/src/utils/translations.tsx
@@ -1,3 +1,4 @@
+'use client';
import { FrameQuery, Locale, Url } from '@custom/schema';
import React, {
createContext,
@@ -14,11 +15,11 @@ import { useOperation } from './operation';
* A list of translations for the given page.
* A translations consists of the locale and the corresponding path.
*/
-export type Translations = Partial>;
+export type TranslationPaths = Partial>;
export const TranslationsContext = createContext<{
- translations: Translations;
- setTranslations: (translations: Translations) => void;
+ translations: TranslationPaths;
+ setTranslations: (translations: TranslationPaths) => void;
}>({
translations: {},
setTranslations: () => {},
@@ -27,8 +28,8 @@ export const TranslationsContext = createContext<{
export function TranslationsProvider({
children,
defaultTranslations,
-}: PropsWithChildren<{ defaultTranslations?: Translations }>) {
- const [translations, setTranslations] = useState(
+}: PropsWithChildren<{ defaultTranslations?: TranslationPaths }>) {
+ const [translations, setTranslations] = useState(
defaultTranslations || {},
);
return (
@@ -47,18 +48,28 @@ function deepCompare(a: any, b: any) {
);
}
-export function useTranslations(newTranslations?: Translations) {
- const homeTranslations = Object.fromEntries(
- useOperation(FrameQuery)
- .data?.websiteSettings?.homePage?.translations?.filter(isTruthy)
- .map(({ locale, path }) => [locale, path]) || [],
- );
+export function Translations({
+ translations: newTranslations,
+ children,
+}: PropsWithChildren<{
+ translations: TranslationPaths;
+}>) {
const { setTranslations, translations } = useContext(TranslationsContext);
useEffect(() => {
if (newTranslations && !deepCompare(translations, newTranslations)) {
setTranslations(newTranslations);
}
}, [setTranslations, newTranslations, translations]);
+ return children;
+}
+
+export function useTranslations() {
+ const homeTranslations = Object.fromEntries(
+ useOperation(FrameQuery)
+ .data?.websiteSettings?.homePage?.translations?.filter(isTruthy)
+ .map(({ locale, path }) => [locale, path]) || [],
+ );
+ const { translations } = useContext(TranslationsContext);
const homePaths = Object.fromEntries(
Object.values(Locale).map((locale) => [locale, `/${locale}` as Url]),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a82cd893..552ea258a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -292,6 +292,12 @@ importers:
react-dom:
specifier: 19.0.0-rc.0
version: 19.0.0-rc.0(react@19.0.0-rc.0)
+ react-error-boundary:
+ specifier: ^4.0.13
+ version: 4.0.13(react@19.0.0-rc.0)
+ react-server-dom-webpack:
+ specifier: 19.0.0-rc.0
+ version: 19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0)
waku:
specifier: 0.21.0-alpha.2
version: 0.21.0-alpha.2(@types/node@20.14.2)(react-dom@19.0.0-rc.0)(react-server-dom-webpack@19.0.0-rc.0)(react@19.0.0-rc.0)
@@ -326,6 +332,9 @@ importers:
react-dom:
specifier: 19.0.0-rc.0
version: 19.0.0-rc.0(react@19.0.0-rc.0)
+ server-only-context:
+ specifier: ^0.1.0
+ version: 0.1.0(react@19.0.0-rc.0)
waku:
specifier: 0.21.0-alpha.2
version: 0.21.0-alpha.2(@types/node@20.14.2)(react-dom@19.0.0-rc.0)(react-server-dom-webpack@19.0.0-rc.0)(react@19.0.0-rc.0)
@@ -482,6 +491,9 @@ importers:
react-intl:
specifier: ^6.6.5
version: 6.6.5(react@19.0.0-rc.0)(typescript@5.3.3)
+ react-server-dom-webpack:
+ specifier: 19.0.0-rc.0
+ version: 19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0)
packages/schema:
dependencies:
@@ -588,6 +600,9 @@ importers:
react-hook-form:
specifier: ^7.49.2
version: 7.49.2(react@19.0.0-rc.0)
+ react-server-dom-webpack:
+ specifier: 19.0.0-rc.0
+ version: 19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0)
swr:
specifier: ^2.2.4
version: 2.2.4(react@19.0.0-rc.0)
@@ -12409,6 +12424,7 @@ packages:
/@vitest/utils@1.6.0:
resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
+ requiresBuild: true
dependencies:
diff-sequences: 29.6.3
estree-walker: 3.0.3
@@ -13155,11 +13171,6 @@ packages:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
engines: {node: '>=0.4.0'}
- /acorn@6.4.2:
- resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==}
- engines: {node: '>=0.4.0'}
- hasBin: true
-
/acorn@7.4.1:
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
engines: {node: '>=0.4.0'}
@@ -18830,6 +18841,7 @@ packages:
/estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+ requiresBuild: true
dependencies:
'@types/estree': 1.0.5
dev: true
@@ -20526,7 +20538,7 @@ packages:
react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.3.3)(webpack@5.91.0)
react-dom: 19.0.0-rc.0(react@19.0.0-rc.0)
react-refresh: 0.14.0
- react-server-dom-webpack: 0.0.0-experimental-c8b778b7f-20220825(react@19.0.0-rc.0)(webpack@5.91.0)
+ react-server-dom-webpack: 19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0)
redux: 4.2.1
redux-thunk: 2.4.2(redux@4.2.1)
resolve-from: 5.0.0
@@ -20731,7 +20743,7 @@ packages:
react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.4.5)(webpack@5.91.0)
react-dom: 19.0.0-rc.0(react@19.0.0-rc.0)
react-refresh: 0.14.2
- react-server-dom-webpack: 0.0.0-experimental-c8b778b7f-20220825(react@19.0.0-rc.0)(webpack@5.91.0)
+ react-server-dom-webpack: 19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0)
redux: 4.2.1
redux-thunk: 2.4.2(redux@4.2.1)
resolve-from: 5.0.0
@@ -23678,6 +23690,7 @@ packages:
/js-tokens@9.0.0:
resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==}
+ requiresBuild: true
dev: true
/js-yaml@3.14.1:
@@ -28655,6 +28668,15 @@ packages:
react-is: 18.1.0
dev: true
+ /react-error-boundary@4.0.13(react@19.0.0-rc.0):
+ resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==}
+ peerDependencies:
+ react: 19.0.0-rc.0
+ dependencies:
+ '@babel/runtime': 7.24.4
+ react: 19.0.0-rc.0
+ dev: false
+
/react-error-overlay@6.0.11:
resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==}
@@ -28957,19 +28979,6 @@ packages:
- '@types/react'
dev: false
- /react-server-dom-webpack@0.0.0-experimental-c8b778b7f-20220825(react@19.0.0-rc.0)(webpack@5.91.0):
- resolution: {integrity: sha512-JyCjbp6ZvkH/T0EuVPdceYlC8u5WqWDSJr2KxDvc81H2eJ+7zYUN++IcEycnR2F+HmER8QVgxfotnIx352zi+w==}
- engines: {node: '>=0.10.0'}
- peerDependencies:
- react: 19.0.0-rc.0
- webpack: ^5.59.0
- dependencies:
- acorn: 6.4.2
- loose-envify: 1.4.0
- neo-async: 2.6.2
- react: 19.0.0-rc.0
- webpack: 5.91.0
-
/react-server-dom-webpack@19.0.0-rc.0(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0)(webpack@5.91.0):
resolution: {integrity: sha512-nnSBQnXKEgfgSx6veKJg3TdRmRyn+tyOuKwKdHCI1SuR+WL2JLDM+NfZrP5DFie7w5ZCNTjS/LdACV4YuRuxDg==}
engines: {node: '>=0.10.0'}
@@ -28983,7 +28992,6 @@ packages:
react: 19.0.0-rc.0
react-dom: 19.0.0-rc.0(react@19.0.0-rc.0)
webpack: 5.91.0
- dev: false
/react-split-pane@0.1.92(react-dom@19.0.0-rc.0)(react@19.0.0-rc.0):
resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==}
@@ -30289,6 +30297,14 @@ packages:
- supports-color
dev: true
+ /server-only-context@0.1.0(react@19.0.0-rc.0):
+ resolution: {integrity: sha512-5Ba19yx9Bj9uSh40aZNsZNBvLF88tLY3na6QxWkqyrSLOSHPxtnzTs/JzjKwfac1NbHFlEodkN7T4M6UzvH2Ng==}
+ peerDependencies:
+ react: 19.0.0-rc.0
+ dependencies:
+ react: 19.0.0-rc.0
+ dev: false
+
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -31324,6 +31340,7 @@ packages:
/strip-literal@2.1.0:
resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==}
+ requiresBuild: true
dependencies:
js-tokens: 9.0.0
dev: true