Skip to content

Commit

Permalink
Add react-router-monorepo template
Browse files Browse the repository at this point in the history
  • Loading branch information
gabro committed Oct 18, 2024
1 parent e94a96d commit 2c2cb8e
Show file tree
Hide file tree
Showing 28 changed files with 669 additions and 0 deletions.
4 changes: 4 additions & 0 deletions templates/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package-lock.json
yarn.lock
pnpm-lock.yaml
pnpm-lock.yml
9 changes: 9 additions & 0 deletions templates/react-router-monorepo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules

/.cache
/build
.env
.react-router

.nx/cache
.nx/workspace-data
4 changes: 4 additions & 0 deletions templates/react-router-monorepo/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
40 changes: 40 additions & 0 deletions templates/react-router-monorepo/apps/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Welcome to React Router!

- 📖 [React Router docs](https://reactrouter.com/dev)

## Development

Run the dev server:

```shellscript
npm run dev
```

## Deployment

First, build your app for production:

```sh
npm run build
```

Then run the app in production mode:

```sh
npm start
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying Node applications, the built-in app server is production-ready.

Make sure to deploy the output of `npm run build`

- `build/server`
- `build/client`

## Styling

This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
56 changes: 56 additions & 0 deletions templates/react-router-monorepo/apps/app/app/defaultMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ComponentProps } from "react";
import { BentoProvider } from "design-system";
import { useTranslation } from "react-i18next";

export const useDefaultMessages = (): ComponentProps<typeof BentoProvider>["defaultMessages"] => {
const { t } = useTranslation();

return {
Chip: {
dismissButtonLabel: t("common.chip.dismissButtonLabel", "Remove"),
},
Banner: {
dismissButtonLabel: t("common.banner.dismissButtonLabel", "Close"),
},
Modal: {
closeButtonLabel: t("common.modal.closeButtonLabel", "Close"),
},
SelectField: {
noOptionsMessage: t("common.selectField.noOptionsMessage", "No options"),
multiOptionsSelected: (n) => {
const options =
n > 1
? t("common.selectField.optionsPlural", "options")
: t("common.selectField.optionsSingular", "option");
return t("common.selectField.multiOptionsSelected", "{{n}} {{options}} selected", {
n,
options,
});
},
selectAllButtonLabel: t("common.selectField.selectAllButtonLabel", "Select all"),
clearAllButtonLabel: t("common.selectField.clearAllButtonLabel", "Clear all"),
},
SearchBar: {
clearButtonLabel: t("common.searchBar.clearButtonLabel", "Clear"),
},
Table: {
noResultsTitle: t("common.table.noResultsTitle", "No results found"),
noResultsDescription: t(
"common.table.noResultsDescription",
"Try adjusting your search filters to find what you're looking for."
),
missingValue: t("common.table.missingValue", "-"),
},
Loader: {
loadingMessage: t("common.loader.loadingMessage", "Loading..."),
},
DateField: {
previousMonthLabel: t("common.dateField.previousMonthLabel", "Prev month"),
nextMonthLabel: t("common.dateField.nextMonthLabel", "Next month"),
},
TextField: {
showPasswordLabel: t("common.textField.showPasswordLabel", "Show password"),
hidePasswordLabel: t("common.textField.hidePasswordLabel", "Hide password"),
},
};
};
44 changes: 44 additions & 0 deletions templates/react-router-monorepo/apps/app/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import i18n, { registerCustomFormats } from "./i18n";
import i18next from "i18next";
import { I18nextProvider, initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";

await i18next
.use(initReactI18next) // Tell i18next to use the react-i18next plugin
.use(LanguageDetector) // Setup a client-side language detector
.use(Backend) // Setup your backend
.init({
...i18n, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
ns: [],
backend: { loadPath: "/locales/{{lng}}.json" },
detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
// we can communicate to the client the language detected server-side
order: ["htmlTag"],
// Because we only use htmlTag, there's no reason to cache the language
// on the browser, so we disable it
caches: [],
},
});

registerCustomFormats(i18next);

console.log(I18nextProvider);
console.log(i18next);

startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<HydratedRouter />
</StrictMode>
</I18nextProvider>
);
});
103 changes: 103 additions & 0 deletions templates/react-router-monorepo/apps/app/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { PassThrough } from "node:stream";

import type { AppLoadContext, EntryContext } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import { createInstance } from "i18next";
import i18next from "./i18next.server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import Backend from "i18next-fs-backend";
import i18n, { registerCustomFormats } from "./i18n"; // your i18n configuration file
import * as path from "node:path";

const ABORT_DELAY = 5_000;

// Override console.erro to suppress specific warnings
const originalConsoleError = console.error;
console.error = (msg, ...args) => {
if (typeof msg === "string" && msg.includes("useLayoutEffect")) {
return;
}
if (typeof msg === "string" && msg.includes("A props object containing")) {
return;
}
originalConsoleError(msg, ...args);
};

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext
) {
return new Promise(async (resolve, reject) => {
let shellRendered = false;
let userAgent = request.headers.get("user-agent");

// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
let readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";

let instance = createInstance();
let lng = await i18next.getLocale(request);
// let ns = i18next.getRouteNamespaces(routerContext);
let ns = ["translation"] as string[];

await instance
.use(initReactI18next) // Tell our instance to use react-i18next
.use(Backend) // Setup our backend
.init({
...i18n, // spread the configuration
lng, // The locale we detected above
ns, // The namespaces the routes about to render wants to use
backend: {
loadPath: path.resolve("./public/locales/{{lng}}.json"),
},
});

registerCustomFormats(instance);

const { pipe, abort } = renderToPipeableStream(
<I18nextProvider i18n={instance}>
<ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />,
</I18nextProvider>,
{
[readyOption]() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set("Content-Type", "text/html");

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}
22 changes: 22 additions & 0 deletions templates/react-router-monorepo/apps/app/app/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { i18n, InitOptions } from "i18next";
import type en from "../public/locales/en.json";

export default {
supportedLngs: ["en", "it"],
fallbackLng: "en",
} satisfies InitOptions;

export function registerCustomFormats(i18n: i18n) {
i18n.services.formatter?.add("capitalize", (value: string) => {
return value.charAt(0).toUpperCase() + value.slice(1);
});
}

declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "translation";
resources: {
translation: typeof en;
};
}
}
25 changes: 25 additions & 0 deletions templates/react-router-monorepo/apps/app/app/i18next.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next/server";
import i18n from "./i18n";

const i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
},
// This is the configuration for i18next used
// when translating messages server-side only
i18next: {
...i18n,
backend: {
loadPath: resolve("./public/locales/{{lng}}.json"),
},
},
// 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],
});

export default i18next;
3 changes: 3 additions & 0 deletions templates/react-router-monorepo/apps/app/app/root.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
height: 100vh;
}
60 changes: 60 additions & 0 deletions templates/react-router-monorepo/apps/app/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Link,
Links,
LinksFunction,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import "design-system/index.css";
import { BentoProvider, Children } from "design-system";
import "./root.css";
import i18next from "./i18next.server";
import type * as Route from "./+types.root";
import { useChangeLanguage } from "remix-i18next/react";
import { useTranslation } from "react-i18next";
import { useDefaultMessages } from "./defaultMessages";

export const links: LinksFunction = () => [];

export async function loader({ request }: Route.LoaderArgs) {
let locale = await i18next.getLocale(request);
return { locale };
}

export function Layout({ children }: { children: React.ReactNode }) {
const { i18n } = useTranslation();
const defaultMessages = useDefaultMessages();
return (
<html lang={i18n.language} dir={i18n.dir()}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<BentoProvider
defaultMessages={defaultMessages}
linkComponent={({ href, ...props }) => {
if (href.startsWith("/")) {
return <Link to={href} {...props} />;
}
return <a href={href} {...props} />;
}}
>
{children as Children}
</BentoProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App({ loaderData }: Route.ComponentProps) {
useChangeLanguage(loaderData.locale);

return <Outlet />;
}
3 changes: 3 additions & 0 deletions templates/react-router-monorepo/apps/app/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type RouteConfig, index } from "@react-router/dev/routes";

export const routes: RouteConfig = [index("routes/index.tsx")];
Loading

0 comments on commit 2c2cb8e

Please sign in to comment.