Skip to content

Commit

Permalink
fix(i18n): use i18next-fetch-backend instead of i18next-http-backend
Browse files Browse the repository at this point in the history
  • Loading branch information
balzdur committed Oct 14, 2024
1 parent c2b9b39 commit 91ebed8
Show file tree
Hide file tree
Showing 41 changed files with 209 additions and 153 deletions.
5 changes: 2 additions & 3 deletions packages/app-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@
"firebase": "^10.13.2",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-fs-backend": "^2.3.2",
"i18next-http-backend": "^2.6.1",
"i18next-fetch-backend": "^6.0.0",
"isbot": "^5.1.17",
"marble-api": "workspace:*",
"match-sorter": "^6.3.4",
"nanoid": "^5.0.7",
"pretty-cache-header": "^1.0.0",
"qs": "^6.13.0",
"react": "^18.3.1",
"react-day-picker": "^9.1.2",
Expand All @@ -97,7 +97,6 @@
"react-i18next": "^15.0.2",
"reactflow": "^11.11.4",
"remeda": "^2.14.0",
"remix": "^2.12.1",
"remix-i18next": "^6.4.1",
"remix-utils": "^7.7.0",
"short-uuid": "^5.2.0",
Expand Down
6 changes: 5 additions & 1 deletion packages/app-builder/src/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export default async function handleRequest(

const App = (
<I18nextProvider i18n={i18n}>
<RemixServer context={remixContext} url={request.url} />
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</I18nextProvider>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"outcome.review": "Review",
"outcome.block_and_review": "Block and Review",
"outcome.decline": "Decline",
"outcome.null": "Null",
"outcome.unknown": "Unknown",
"review_status.pending": "pending",
"review_status.approve": "approve",
Expand Down
95 changes: 45 additions & 50 deletions packages/app-builder/src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type ShouldRevalidateFunctionArgs,
useLoaderData,
useRouteError,
useRouteLoaderData,
} from '@remix-run/react';
import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix';
import { type Namespace } from 'i18next';
Expand Down Expand Up @@ -118,80 +119,74 @@ export const meta: MetaFunction = () => [
},
];

export function ErrorBoundary() {
const error = useRouteError();
captureRemixErrorBoundaryError(error);

return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body className="selection:text-grey-00 h-screen w-full overflow-hidden antialiased selection:bg-purple-100">
<div className="from-purple-10 to-grey-02 flex size-full flex-col items-center bg-gradient-to-r">
<div className="flex size-full flex-col items-center bg-no-repeat">
<div className="flex h-full max-h-80 flex-col justify-center">
<Link to={getRoute('/sign-in')}>
<Logo
logo="logo-standard"
className="h-10 w-auto text-[#080525]"
preserveAspectRatio="xMinYMid meet"
aria-labelledby="marble"
/>
</Link>
</div>
<div className="bg-grey-00 mb-10 flex shrink-0 rounded-2xl p-10 text-center shadow-md">
<ErrorComponent error={error} />
</div>
</div>
</div>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

function App() {
const { locale, ENV, toastMessage, csrf, segmentScript } =
useLoaderData<typeof loader>();

const { i18n } = useTranslation(handle.i18n);

useChangeLanguage(locale);
export function Layout({ children }: { children: React.ReactNode }) {
const loaderData = useRouteLoaderData<typeof loader>('root');

const { i18n } = useTranslation();
useSegmentPageTracking();

return (
<html lang={locale} dir={i18n.dir()}>
<html lang={loaderData?.locale ?? 'en'} dir={i18n.dir()}>
<head>
<Meta />
<Links />
{segmentScript ? <SegmentScript script={segmentScript} /> : null}
{loaderData?.segmentScript ? (
<SegmentScript script={loaderData.segmentScript} />
) : null}
<ExternalScripts />
</head>
<body className="selection:text-grey-00 h-screen w-full overflow-hidden antialiased selection:bg-purple-100">
<AuthenticityTokenProvider token={csrf}>
<Tooltip.Provider>
<Outlet />
</Tooltip.Provider>
<AuthenticityTokenProvider token={loaderData?.csrf ?? ''}>
<Tooltip.Provider>{children}</Tooltip.Provider>
</AuthenticityTokenProvider>
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(ENV)}`,
__html: `window.ENV = ${JSON.stringify(loaderData?.ENV ?? {})}`,
}}
/>
<ScrollRestoration />
<Scripts />
<ClientOnly>
{() => <MarbleToaster toastMessage={toastMessage} />}
{() => <MarbleToaster toastMessage={loaderData?.toastMessage} />}
</ClientOnly>
</body>
</html>
);
}

export function ErrorBoundary() {
const error = useRouteError();
captureRemixErrorBoundaryError(error);

return (
<div className="from-purple-10 to-grey-02 flex size-full flex-col items-center bg-gradient-to-r">
<div className="flex size-full flex-col items-center bg-no-repeat">
<div className="flex h-full max-h-80 flex-col justify-center">
<Link to={getRoute('/sign-in')}>
<Logo
logo="logo-standard"
className="h-10 w-auto text-[#080525]"
preserveAspectRatio="xMinYMid meet"
aria-labelledby="marble"
/>
</Link>
</div>
<div className="bg-grey-00 mb-10 flex shrink-0 rounded-2xl p-10 text-center shadow-md">
<ErrorComponent error={error} />
</div>
</div>
</div>
);
}

function App() {
const { locale } = useLoaderData<typeof loader>();

useChangeLanguage(locale);

return <Outlet />;
}

export default withSentry(App);

export function shouldRevalidate({
Expand Down
38 changes: 38 additions & 0 deletions packages/app-builder/src/routes/ressources+/locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { supportedLngs } from '@app-builder/services/i18n/i18n-config';
import { resources } from '@app-builder/services/i18n/resources/resources.server';
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { cacheHeader } from 'pretty-cache-header';
import { z } from 'zod';

export function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);

const lng = z.enum(supportedLngs).parse(url.searchParams.get('lng'));

const namespaces = resources[lng];

const ns = z
.string()
.refine((ns): ns is keyof typeof namespaces => {
return Object.keys(resources[lng]).includes(ns);
})
.parse(url.searchParams.get('ns'));

const headers = new Headers();

// On production, we want to add cache headerlocals to the response
// eslint-disable-next-line no-restricted-properties
if (process.env.NODE_ENV === 'production') {
headers.set(
'Cache-Control',
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
cacheHeader({
maxAge: '1d',
staleWhileRevalidate: '7d',
staleIfError: '7d',
}),
);
}

return json(namespaces[ns], { headers });
}
18 changes: 10 additions & 8 deletions packages/app-builder/src/services/i18n/i18next.client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend, { type HttpBackendOptions } from 'i18next-http-backend';
import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector';
import Fetch from 'i18next-fetch-backend';
import { initReactI18next } from 'react-i18next';
import { getInitialNamespaces } from 'remix-i18next/client';

Expand All @@ -11,15 +11,12 @@ export function makeI18nextClientService() {
// eslint-disable-next-line import/no-named-as-default-member
await i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init<HttpBackendOptions>({
.use(Fetch)
.use(I18nextBrowserLanguageDetector)
.init({
...i18nConfig,
// This function detects the namespaces your routes rendered while SSR use
ns: getInitialNamespaces(),
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
Expand All @@ -29,6 +26,11 @@ export function makeI18nextClientService() {
// on the browser, so we disable it
caches: [],
},
backend: {
// We will configure the backend to fetch the translations from the
// resource route /api/locales and pass the lng and ns as search params
loadPath: '/ressources/locales?lng={{lng}}&ns={{ns}}',
},
});

return i18next;
Expand Down
35 changes: 2 additions & 33 deletions packages/app-builder/src/services/i18n/i18next.d.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,8 @@
import type api from '../../../public/locales/en/api.json';
import type auth from '../../../public/locales/en/auth.json';
import type cases from '../../../public/locales/en/cases.json';
import type common from '../../../public/locales/en/common.json';
import type data from '../../../public/locales/en/data.json';
import type decisions from '../../../public/locales/en/decisions.json';
import type filters from '../../../public/locales/en/filters.json';
import type lists from '../../../public/locales/en/lists.json';
import type navigation from '../../../public/locales/en/navigation.json';
import type scenarios from '../../../public/locales/en/scenarios.json';
import type scheduledExecution from '../../../public/locales/en/scheduledExecution.json';
import type settings from '../../../public/locales/en/settings.json';
import type transfercheck from '../../../public/locales/en/transfercheck.json';
import type upload from '../../../public/locales/en/upload.json';
import type workflows from '../../../public/locales/en/workflows.json';
import { type defaultNS } from './i18n-config';
import { type resources } from './resources/resources.server';

declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: {
api: typeof api;
cases: typeof cases;
common: typeof common;
data: typeof data;
decisions: typeof decisions;
filters: typeof filters;
navigation: typeof navigation;
lists: typeof lists;
auth: typeof auth;
scenarios: typeof scenarios;
scheduledExecution: typeof scheduledExecution;
settings: typeof settings;
transfercheck: typeof transfercheck;
upload: typeof upload;
workflows: typeof workflows;
};
resources: (typeof resources)['en'];
}
}
29 changes: 8 additions & 21 deletions packages/app-builder/src/services/i18n/i18next.server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { resolve } from 'node:path';

import { type LngStorageRepository } from '@app-builder/repositories/SessionStorageRepositories/LngStorageRepository';
import { type EntryContext } from '@remix-run/node';
import { createInstance, type FlatNamespace, type InitOptions } from 'i18next';
import Backend from 'i18next-fs-backend';
import { initReactI18next } from 'react-i18next';
import { RemixI18Next } from 'remix-i18next/server';

import { i18nConfig } from './i18n-config';
import { resources } from './resources/resources.server';

export function makeI18nextServerService({ lngStorage }: LngStorageRepository) {
const remixI18next = new RemixI18Next({
Expand All @@ -20,14 +18,8 @@ export function makeI18nextServerService({ lngStorage }: LngStorageRepository) {
// when translating messages server-side only
i18next: {
...i18nConfig,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
resources,
},
// The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
// E.g. The Backend plugin for loading translations from the file system
// Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
plugins: [Backend],
});

async function getI18nextServerInstance(
Expand All @@ -43,17 +35,12 @@ export function makeI18nextServerService({ lngStorage }: LngStorageRepository) {
// And here we detect what namespaces the routes about to render want to use
const ns = remixI18next.getRouteNamespaces(remixContext);

await instance
.use(initReactI18next)
.use(Backend)
.init({
...i18nConfig,
lng,
ns,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
});
await instance.use(initReactI18next).init({
...i18nConfig,
resources,
lng,
ns,
});

return instance;
}
Expand Down
33 changes: 33 additions & 0 deletions packages/app-builder/src/services/i18n/resources/en.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import api from '@app-builder/locales/en/api.json';
import auth from '@app-builder/locales/en/auth.json';
import cases from '@app-builder/locales/en/cases.json';
import common from '@app-builder/locales/en/common.json';
import data from '@app-builder/locales/en/data.json';
import decisions from '@app-builder/locales/en/decisions.json';
import filters from '@app-builder/locales/en/filters.json';
import lists from '@app-builder/locales/en/lists.json';
import navigation from '@app-builder/locales/en/navigation.json';
import scenarios from '@app-builder/locales/en/scenarios.json';
import scheduledExecution from '@app-builder/locales/en/scheduledExecution.json';
import settings from '@app-builder/locales/en/settings.json';
import transfercheck from '@app-builder/locales/en/transfercheck.json';
import upload from '@app-builder/locales/en/upload.json';
import workflows from '@app-builder/locales/en/workflows.json';

export const en = {
api,
cases,
common,
data,
decisions,
filters,
navigation,
lists,
auth,
scenarios,
scheduledExecution,
settings,
transfercheck,
upload,
workflows,
};
Loading

0 comments on commit 91ebed8

Please sign in to comment.