Skip to content

Commit 0a14aa4

Browse files
authored
docs(examples): add Remix Vite + Babel example (#1946)
* docs(examples): add Remix Vite + Babel example * docs(examples): add example with translation from loader * docs(examples): add dynamic loading of catalog when locale changes
1 parent 0de6a26 commit 0a14aa4

File tree

20 files changed

+10592
-0
lines changed

20 files changed

+10592
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: "latest",
12+
sourceType: "module",
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
ignorePatterns: ["!**/.server", "!**/.client"],
23+
24+
// Base config
25+
extends: ["eslint:recommended"],
26+
27+
overrides: [
28+
// React
29+
{
30+
files: ["**/*.{js,jsx,ts,tsx}"],
31+
plugins: ["react", "jsx-a11y"],
32+
extends: [
33+
"plugin:react/recommended",
34+
"plugin:react/jsx-runtime",
35+
"plugin:react-hooks/recommended",
36+
"plugin:jsx-a11y/recommended",
37+
],
38+
settings: {
39+
react: {
40+
version: "detect",
41+
},
42+
formComponents: ["Form"],
43+
linkComponents: [
44+
{ name: "Link", linkAttribute: "to" },
45+
{ name: "NavLink", linkAttribute: "to" },
46+
],
47+
"import/resolver": {
48+
typescript: {},
49+
},
50+
},
51+
},
52+
53+
// Typescript
54+
{
55+
files: ["**/*.{ts,tsx}"],
56+
plugins: ["@typescript-eslint", "import"],
57+
parser: "@typescript-eslint/parser",
58+
settings: {
59+
"import/internal-regex": "^~/",
60+
"import/resolver": {
61+
node: {
62+
extensions: [".ts", ".tsx"],
63+
},
64+
typescript: {
65+
alwaysTryTypes: true,
66+
},
67+
},
68+
},
69+
extends: [
70+
"plugin:@typescript-eslint/recommended",
71+
"plugin:import/recommended",
72+
"plugin:import/typescript",
73+
],
74+
},
75+
76+
// Node
77+
{
78+
files: [".eslintrc.cjs"],
79+
env: {
80+
node: true,
81+
},
82+
},
83+
],
84+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.yarn
2+
node_modules
3+
4+
/.cache
5+
/build
6+
.env
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Welcome to Remix + Vite!
2+
3+
📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/guides/vite) for details on supported features.
4+
5+
## Development
6+
7+
Run the Vite 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 Remix app server is production-ready.
32+
33+
Make sure to deploy the output of `npm run build`
34+
35+
- `build/server`
36+
- `build/client`
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* By default, Remix will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.client
5+
*/
6+
7+
import { i18n } from "@lingui/core";
8+
import { detect, fromHtmlTag } from "@lingui/detect-locale";
9+
import { I18nProvider } from "@lingui/react";
10+
import { RemixBrowser } from "@remix-run/react";
11+
import { startTransition, StrictMode } from "react";
12+
import { hydrateRoot } from "react-dom/client";
13+
import { loadCatalog } from "./modules/lingui/lingui";
14+
15+
async function main() {
16+
const locale = detect(fromHtmlTag("lang")) || "en";
17+
18+
await loadCatalog(locale);
19+
20+
startTransition(() => {
21+
hydrateRoot(
22+
document,
23+
<StrictMode>
24+
<I18nProvider i18n={i18n}>
25+
<RemixBrowser />
26+
</I18nProvider>
27+
</StrictMode>
28+
);
29+
});
30+
}
31+
32+
main()
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* By default, Remix will handle generating the HTTP Response for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.server
5+
*/
6+
7+
import { PassThrough } from "node:stream";
8+
9+
import { i18n } from "@lingui/core";
10+
import { I18nProvider } from "@lingui/react";
11+
import type { AppLoadContext, EntryContext } from "@remix-run/node";
12+
import { createReadableStreamFromReadable } from "@remix-run/node";
13+
import { RemixServer } from "@remix-run/react";
14+
import { isbot } from "isbot";
15+
import { renderToPipeableStream } from "react-dom/server";
16+
import { linguiServer } from "./modules/lingui/lingui.server";
17+
import { loadCatalog } from "./modules/lingui/lingui";
18+
19+
const ABORT_DELAY = 5_000;
20+
21+
export default function handleRequest(
22+
request: Request,
23+
responseStatusCode: number,
24+
responseHeaders: Headers,
25+
remixContext: EntryContext,
26+
// This is ignored so we can keep it in the template for visibility. Feel
27+
// free to delete this parameter in your app if you're not using it!
28+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
29+
loadContext: AppLoadContext
30+
) {
31+
return isbot(request.headers.get("user-agent") || "")
32+
? handleBotRequest(
33+
request,
34+
responseStatusCode,
35+
responseHeaders,
36+
remixContext
37+
)
38+
: handleBrowserRequest(
39+
request,
40+
responseStatusCode,
41+
responseHeaders,
42+
remixContext
43+
);
44+
}
45+
46+
async function handleBotRequest(
47+
request: Request,
48+
responseStatusCode: number,
49+
responseHeaders: Headers,
50+
remixContext: EntryContext
51+
) {
52+
const locale = await linguiServer.getLocale(request);
53+
await loadCatalog(locale);
54+
55+
return new Promise((resolve, reject) => {
56+
let shellRendered = false;
57+
const { pipe, abort } = renderToPipeableStream(
58+
<I18nProvider i18n={i18n}>
59+
<RemixServer
60+
context={remixContext}
61+
url={request.url}
62+
abortDelay={ABORT_DELAY}
63+
/>
64+
</I18nProvider>,
65+
{
66+
onAllReady() {
67+
shellRendered = true;
68+
const body = new PassThrough();
69+
const stream = createReadableStreamFromReadable(body);
70+
71+
responseHeaders.set("Content-Type", "text/html");
72+
73+
resolve(
74+
new Response(stream, {
75+
headers: responseHeaders,
76+
status: responseStatusCode,
77+
})
78+
);
79+
80+
pipe(body);
81+
},
82+
onShellError(error: unknown) {
83+
reject(error);
84+
},
85+
onError(error: unknown) {
86+
responseStatusCode = 500;
87+
// Log streaming rendering errors from inside the shell. Don't log
88+
// errors encountered during initial shell rendering since they'll
89+
// reject and get logged in handleDocumentRequest.
90+
if (shellRendered) {
91+
console.error(error);
92+
}
93+
},
94+
}
95+
);
96+
97+
setTimeout(abort, ABORT_DELAY);
98+
});
99+
}
100+
101+
async function handleBrowserRequest(
102+
request: Request,
103+
responseStatusCode: number,
104+
responseHeaders: Headers,
105+
remixContext: EntryContext
106+
) {
107+
const locale = await linguiServer.getLocale(request);
108+
await loadCatalog(locale);
109+
110+
return new Promise((resolve, reject) => {
111+
let shellRendered = false;
112+
const { pipe, abort } = renderToPipeableStream(
113+
<I18nProvider i18n={i18n}>
114+
<RemixServer
115+
context={remixContext}
116+
url={request.url}
117+
abortDelay={ABORT_DELAY}
118+
/>
119+
</I18nProvider>,
120+
{
121+
onShellReady() {
122+
shellRendered = true;
123+
const body = new PassThrough();
124+
const stream = createReadableStreamFromReadable(body);
125+
126+
responseHeaders.set("Content-Type", "text/html");
127+
128+
resolve(
129+
new Response(stream, {
130+
headers: responseHeaders,
131+
status: responseStatusCode,
132+
})
133+
);
134+
135+
pipe(body);
136+
},
137+
onShellError(error: unknown) {
138+
reject(error);
139+
},
140+
onError(error: unknown) {
141+
responseStatusCode = 500;
142+
// Log streaming rendering errors from inside the shell. Don't log
143+
// errors encountered during initial shell rendering since they'll
144+
// reject and get logged in handleDocumentRequest.
145+
if (shellRendered) {
146+
console.error(error);
147+
}
148+
},
149+
}
150+
);
151+
152+
setTimeout(abort, ABORT_DELAY);
153+
});
154+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
msgid ""
2+
msgstr ""
3+
"POT-Creation-Date: 2024-05-29 14:28+0200\n"
4+
"MIME-Version: 1.0\n"
5+
"Content-Type: text/plain; charset=utf-8\n"
6+
"Content-Transfer-Encoding: 8bit\n"
7+
"X-Generator: @lingui/cli\n"
8+
"Language: en\n"
9+
"Project-Id-Version: \n"
10+
"Report-Msgid-Bugs-To: \n"
11+
"PO-Revision-Date: \n"
12+
"Last-Translator: \n"
13+
"Language-Team: \n"
14+
"Plural-Forms: \n"
15+
16+
#: app/routes/_index.tsx:29
17+
msgid "15m Quickstart Blog Tutorial"
18+
msgstr ""
19+
20+
#: app/routes/_index.tsx:7
21+
msgid "An Unexpected Error Occured"
22+
msgstr ""
23+
24+
#: app/routes/_index.tsx:38
25+
msgid "Deep Dive Jokes App Tutorial"
26+
msgstr ""
27+
28+
#: app/modules/lingui/lingui.tsx:14
29+
msgid "English"
30+
msgstr ""
31+
32+
#: app/modules/lingui/lingui.tsx:15
33+
msgid "French"
34+
msgstr ""
35+
36+
#: app/routes/_index.tsx:14
37+
msgid "New Remix App"
38+
msgstr ""
39+
40+
#: app/routes/_index.tsx:43
41+
msgid "Remix Docs"
42+
msgstr ""
43+
44+
#: app/routes/_index.tsx:21
45+
msgid "Welcome to Remix"
46+
msgstr ""
47+
48+
#: app/routes/_index.tsx:8
49+
msgid "Welcome to Remix!"
50+
msgstr ""

0 commit comments

Comments
 (0)