Skip to content

Commit 424cb3e

Browse files
authored
Merge pull request #924 from buildo/template
Add react-router-monorepo template
2 parents e94a96d + 2d6082d commit 424cb3e

31 files changed

+734
-0
lines changed

.github/workflows/release.yml

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ jobs:
3535
- run: |
3636
cd packages/bento-design-system
3737
pnpm version --no-git-tag-version --new-version ${{ github.ref_name }}
38+
new_version=$(cat package.json | jq -r '.version')
39+
cd ../../templates/react-router-monorepo/libs/design-system
40+
jq '.dependencies["@buildo/bento-design-system"] = "'${new_version}'"' package.json > package.json.tmp && mv package.json.tmp package.json
3841
3942
- name: Commit & Push changes
4043
uses: actions-js/push@master

templates/.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package-lock.json
2+
yarn.lock
3+
pnpm-lock.yaml
4+
pnpm-lock.yml

templates/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Bento project templates
2+
3+
This directory contains project templates that can be used to bootstrap new projects using Bento.
4+
5+
## How to use a template
6+
7+
```bash
8+
pnpx degit buildo/bento-design-system/templates/<template-name> [my-new-project]
9+
```
10+
11+
Omitting the project name will clone the template in the current directory.
12+
13+
## Available templates
14+
15+
- [react-router-monorepo](./react-router-monorepo/README.md): sets up a monorepo using pnpm and Nx. The monorepo contains an app and a design system library. The app uses React Router v7 (with SSR enabled) and it comes with i18n pre-configured.
16+
17+
```bash
18+
pnpx degit buildo/bento-design-system/templates/react-router-monorepo my-new-project
19+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
.env
6+
.react-router
7+
8+
.nx/cache
9+
.nx/workspace-data
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"typescript.tsdk": "apps/app/node_modules/typescript/lib",
3+
"typescript.enablePromptUseWorkspaceTsdk": true
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Welcome to React Router!
2+
3+
- 📖 [React Router docs](https://reactrouter.com/dev)
4+
5+
## Development
6+
7+
Run the dev server:
8+
9+
```shellscript
10+
npm run dev
11+
```
12+
13+
## Deployment
14+
15+
First, build your app for production:
16+
17+
```sh
18+
npm run build
19+
```
20+
21+
Then run the app in production mode:
22+
23+
```sh
24+
npm start
25+
```
26+
27+
Now you'll need to pick a host to deploy it to.
28+
29+
### DIY
30+
31+
If you're familiar with deploying Node applications, the built-in app server is production-ready.
32+
33+
Make sure to deploy the output of `npm run build`
34+
35+
- `build/server`
36+
- `build/client`
37+
38+
## Styling
39+
40+
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ComponentProps } from "react";
2+
import { BentoProvider } from "design-system";
3+
import { useTranslation } from "react-i18next";
4+
5+
export const useDefaultMessages = (): ComponentProps<typeof BentoProvider>["defaultMessages"] => {
6+
const { t } = useTranslation();
7+
8+
return {
9+
Chip: {
10+
dismissButtonLabel: t("common.chip.dismissButtonLabel", "Remove"),
11+
},
12+
Banner: {
13+
dismissButtonLabel: t("common.banner.dismissButtonLabel", "Close"),
14+
},
15+
Modal: {
16+
closeButtonLabel: t("common.modal.closeButtonLabel", "Close"),
17+
},
18+
SelectField: {
19+
noOptionsMessage: t("common.selectField.noOptionsMessage", "No options"),
20+
multiOptionsSelected: (n) => {
21+
const options =
22+
n > 1
23+
? t("common.selectField.optionsPlural", "options")
24+
: t("common.selectField.optionsSingular", "option");
25+
return t("common.selectField.multiOptionsSelected", "{{n}} {{options}} selected", {
26+
n,
27+
options,
28+
});
29+
},
30+
selectAllButtonLabel: t("common.selectField.selectAllButtonLabel", "Select all"),
31+
clearAllButtonLabel: t("common.selectField.clearAllButtonLabel", "Clear all"),
32+
},
33+
SearchBar: {
34+
clearButtonLabel: t("common.searchBar.clearButtonLabel", "Clear"),
35+
},
36+
Table: {
37+
noResultsTitle: t("common.table.noResultsTitle", "No results found"),
38+
noResultsDescription: t(
39+
"common.table.noResultsDescription",
40+
"Try adjusting your search filters to find what you're looking for."
41+
),
42+
missingValue: t("common.table.missingValue", "-"),
43+
},
44+
Loader: {
45+
loadingMessage: t("common.loader.loadingMessage", "Loading..."),
46+
},
47+
DateField: {
48+
previousMonthLabel: t("common.dateField.previousMonthLabel", "Prev month"),
49+
nextMonthLabel: t("common.dateField.nextMonthLabel", "Next month"),
50+
},
51+
TextField: {
52+
showPasswordLabel: t("common.textField.showPasswordLabel", "Show password"),
53+
hidePasswordLabel: t("common.textField.hidePasswordLabel", "Hide password"),
54+
},
55+
};
56+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { startTransition, StrictMode } from "react";
2+
import { hydrateRoot } from "react-dom/client";
3+
import { HydratedRouter } from "react-router/dom";
4+
import i18n, { registerCustomFormats } from "./i18n";
5+
import i18next from "i18next";
6+
import { I18nextProvider, initReactI18next } from "react-i18next";
7+
import LanguageDetector from "i18next-browser-languagedetector";
8+
import Backend from "i18next-http-backend";
9+
10+
await i18next
11+
.use(initReactI18next) // Tell i18next to use the react-i18next plugin
12+
.use(LanguageDetector) // Setup a client-side language detector
13+
.use(Backend) // Setup your backend
14+
.init({
15+
...i18n, // spread the configuration
16+
// This function detects the namespaces your routes rendered while SSR use
17+
ns: [],
18+
backend: { loadPath: "/locales/{{lng}}.json" },
19+
detection: {
20+
// Here only enable htmlTag detection, we'll detect the language only
21+
// server-side with remix-i18next, by using the `<html lang>` attribute
22+
// we can communicate to the client the language detected server-side
23+
order: ["htmlTag"],
24+
// Because we only use htmlTag, there's no reason to cache the language
25+
// on the browser, so we disable it
26+
caches: [],
27+
},
28+
});
29+
30+
registerCustomFormats(i18next);
31+
32+
startTransition(() => {
33+
hydrateRoot(
34+
document,
35+
<I18nextProvider i18n={i18next}>
36+
<StrictMode>
37+
<HydratedRouter />
38+
</StrictMode>
39+
</I18nextProvider>
40+
);
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { PassThrough } from "node:stream";
2+
3+
import type { AppLoadContext, EntryContext } from "react-router";
4+
import { createReadableStreamFromReadable } from "@react-router/node";
5+
import { ServerRouter } from "react-router";
6+
import { isbot } from "isbot";
7+
import type { RenderToPipeableStreamOptions } from "react-dom/server";
8+
import { renderToPipeableStream } from "react-dom/server";
9+
import { createInstance } from "i18next";
10+
import i18next from "./i18next.server";
11+
import { I18nextProvider, initReactI18next } from "react-i18next";
12+
import Backend from "i18next-fs-backend";
13+
import i18n, { registerCustomFormats } from "./i18n"; // your i18n configuration file
14+
import * as path from "node:path";
15+
16+
const ABORT_DELAY = 5_000;
17+
18+
// Override console.erro to suppress specific warnings
19+
const originalConsoleError = console.error;
20+
console.error = (msg, ...args) => {
21+
if (typeof msg === "string" && msg.includes("useLayoutEffect")) {
22+
return;
23+
}
24+
if (typeof msg === "string" && msg.includes("A props object containing")) {
25+
return;
26+
}
27+
originalConsoleError(msg, ...args);
28+
};
29+
30+
export default function handleRequest(
31+
request: Request,
32+
responseStatusCode: number,
33+
responseHeaders: Headers,
34+
routerContext: EntryContext,
35+
loadContext: AppLoadContext
36+
) {
37+
return new Promise(async (resolve, reject) => {
38+
let shellRendered = false;
39+
let userAgent = request.headers.get("user-agent");
40+
41+
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
42+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
43+
let readyOption: keyof RenderToPipeableStreamOptions =
44+
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";
45+
46+
let instance = createInstance();
47+
let lng = await i18next.getLocale(request);
48+
// let ns = i18next.getRouteNamespaces(routerContext);
49+
let ns = ["translation"] as string[];
50+
51+
await instance
52+
.use(initReactI18next) // Tell our instance to use react-i18next
53+
.use(Backend) // Setup our backend
54+
.init({
55+
...i18n, // spread the configuration
56+
lng, // The locale we detected above
57+
ns, // The namespaces the routes about to render wants to use
58+
backend: {
59+
loadPath: path.resolve("./public/locales/{{lng}}.json"),
60+
},
61+
});
62+
63+
registerCustomFormats(instance);
64+
65+
const { pipe, abort } = renderToPipeableStream(
66+
<I18nextProvider i18n={instance}>
67+
<ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />,
68+
</I18nextProvider>,
69+
{
70+
[readyOption]() {
71+
shellRendered = true;
72+
const body = new PassThrough();
73+
const stream = createReadableStreamFromReadable(body);
74+
75+
responseHeaders.set("Content-Type", "text/html");
76+
77+
resolve(
78+
new Response(stream, {
79+
headers: responseHeaders,
80+
status: responseStatusCode,
81+
})
82+
);
83+
84+
pipe(body);
85+
},
86+
onShellError(error: unknown) {
87+
reject(error);
88+
},
89+
onError(error: unknown) {
90+
responseStatusCode = 500;
91+
// Log streaming rendering errors from inside the shell. Don't log
92+
// errors encountered during initial shell rendering since they'll
93+
// reject and get logged in handleDocumentRequest.
94+
if (shellRendered) {
95+
console.error(error);
96+
}
97+
},
98+
}
99+
);
100+
101+
setTimeout(abort, ABORT_DELAY);
102+
});
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { i18n, InitOptions } from "i18next";
2+
import type en from "../public/locales/en.json";
3+
4+
export default {
5+
supportedLngs: ["en", "it"],
6+
fallbackLng: "en",
7+
} satisfies InitOptions;
8+
9+
export function registerCustomFormats(i18n: i18n) {
10+
i18n.services.formatter?.add("capitalize", (value: string) => {
11+
return value.charAt(0).toUpperCase() + value.slice(1);
12+
});
13+
}
14+
15+
declare module "i18next" {
16+
interface CustomTypeOptions {
17+
defaultNS: "translation";
18+
resources: {
19+
translation: typeof en;
20+
};
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Backend from "i18next-fs-backend";
2+
import { resolve } from "node:path";
3+
import { RemixI18Next } from "remix-i18next/server";
4+
import i18n from "./i18n";
5+
6+
const i18next = new RemixI18Next({
7+
detection: {
8+
supportedLanguages: i18n.supportedLngs,
9+
fallbackLanguage: i18n.fallbackLng,
10+
},
11+
// This is the configuration for i18next used
12+
// when translating messages server-side only
13+
i18next: {
14+
...i18n,
15+
backend: {
16+
loadPath: resolve("./public/locales/{{lng}}.json"),
17+
},
18+
},
19+
// The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
20+
// E.g. The Backend plugin for loading translations from the file system
21+
// Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
22+
plugins: [Backend],
23+
});
24+
25+
export default i18next;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
height: 100vh;
3+
}

0 commit comments

Comments
 (0)