diff --git a/examples/rspack/.gitignore b/examples/rspack/.gitignore new file mode 100644 index 000000000..be1301350 --- /dev/null +++ b/examples/rspack/.gitignore @@ -0,0 +1,155 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/rspack/LICENSE b/examples/rspack/LICENSE new file mode 100644 index 000000000..69dae43f9 --- /dev/null +++ b/examples/rspack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Collin Brown + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/rspack/README.md b/examples/rspack/README.md new file mode 100644 index 000000000..95e8eb8dd --- /dev/null +++ b/examples/rspack/README.md @@ -0,0 +1,29 @@ +# Lingui + RSpack Example + +This project shows how to use the [Rspack JavaScript bundler](https://www.rspack.dev/guide/introduction.html) with [Lingui JS](https://lingui.dev/) to provide i18n for a React application (TypeScript). + +![lingui-rspack-i18n-demo](demo.gif) + +## Setup Instructions + +1. `cd rspack-project && npm install` +2. `npm run dev` to run the development server. +3. `npm run build` to build the application. + +## Update I18n + +1. Wrap any messages requiring translation in `` or a related macro. +2. `npm run extract` to generate message catalogs in `src/locales/{locale}/messages`. +3. Translate any new messages in the catalogs. +4. `npm run compile` to create runtime catalogs. + +## Configuration File Notes + +- [rspack.config.js](./rspack-project/rspack.config.js) specifies that that babel should transcompile all `.tsx` files using the `@babel/preset-typscript` and `@babel/preset-react` [presets](https://babeljs.io/docs/presets), as well as the `macros` [plugin](https://babeljs.io/docs/plugins). This step is necessary so that [Lingui Macros](https://lingui.dev/ref/macro) such as `` are correctly transcompiled into their respective React components. +- [lingui.config.ts](./rspack-project/lingui.config.ts) specifies the available locales, defaults, and paths where the message catalogs are stored. +- As per the [Rspack documentation](https://www.rspack.dev/guide/loader.html#builtinswc-loader), `builtin:swc-loader` does not currently support plugins, which is why the trans-compilation work is still done in babel. Once SWC plugins are supported, transcompilation should be done with Rspack's `builtin:swc-loader` for improved performance. + +## Helpful Resources + +- This [blog post](https://betterprogramming.pub/react-app-internationalization-with-linguijs-9486ccd80e07) shows a step-by-step guide to set up LinguiJS with React. +- [Official documentation for React setup with LinguiJS](https://lingui.dev/tutorials/react). This repo closely follows the example from the official docs. \ No newline at end of file diff --git a/examples/rspack/demo.gif b/examples/rspack/demo.gif new file mode 100644 index 000000000..406988398 Binary files /dev/null and b/examples/rspack/demo.gif differ diff --git a/examples/rspack/index.html b/examples/rspack/index.html new file mode 100644 index 000000000..f04606f82 --- /dev/null +++ b/examples/rspack/index.html @@ -0,0 +1,14 @@ + + + + + + + Rspack + React + TS + + + +
+ + + \ No newline at end of file diff --git a/examples/rspack/lingui.config.ts b/examples/rspack/lingui.config.ts new file mode 100644 index 000000000..bff4df262 --- /dev/null +++ b/examples/rspack/lingui.config.ts @@ -0,0 +1,13 @@ +import { LinguiConfig } from '@lingui/conf' + +const config: Partial = { + locales: ["en", "fr"], + sourceLocale: "en", + catalogs: [{ + path: "src/locales/{locale}/messages", + include: ["src"] + }], + format: "po" +}; + +export default config; \ No newline at end of file diff --git a/examples/rspack/package.json b/examples/rspack/package.json new file mode 100644 index 000000000..0d340aae2 --- /dev/null +++ b/examples/rspack/package.json @@ -0,0 +1,34 @@ +{ + "name": "rspack-react-ts-starter", + "private": true, + "version": "1.0.0", + "babel": { + "plugins": [ + "macros" + ] + }, + "scripts": { + "dev": "rspack serve", + "build": "rspack build", + "extract": "lingui extract", + "compile": "lingui compile --typescript" + }, + "dependencies": { + "@babel/preset-react": "^7.22.5", + "@babel/preset-typescript": "^7.22.5", + "@lingui/react": "^4.4.0", + "babel-loader": "^9.1.3", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@babel/core": "^7.22.10", + "@lingui/cli": "^4.4.0", + "@lingui/macro": "^4.4.0", + "@rspack/cli": "latest", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.1", + "babel-plugin-macros": "^3.1.0", + "typescript": "^5.0.4" + } +} diff --git a/examples/rspack/rspack.config.js b/examples/rspack/rspack.config.js new file mode 100644 index 000000000..3e42c07e2 --- /dev/null +++ b/examples/rspack/rspack.config.js @@ -0,0 +1,40 @@ +/** + * @type {import('@rspack/cli').Configuration} + */ +module.exports = { + context: __dirname, + entry: { + main: "./src/main.tsx" + }, + builtins: { + html: [ + { + template: "./index.html" + } + ] + }, + module: { + rules: [ + { + test: /\.svg$/, + type: "asset" + }, + { + test: /\.tsx$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [ + "@babel/preset-typescript", + "@babel/preset-react" + ], + plugins: [ + "macros" + ] + } + } + } + ] + } +}; diff --git a/examples/rspack/src/Inbox.tsx b/examples/rspack/src/Inbox.tsx new file mode 100644 index 000000000..bbb99848e --- /dev/null +++ b/examples/rspack/src/Inbox.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +import { useLingui } from "@lingui/react"; + +import { Trans, Plural } from "@lingui/macro"; + +import LocaleSwitcher from './LocaleSwitcher'; + +export default function Inbox() { + const messages = [{}, {}]; + const messagesCount = messages.length; + const lastLogin = new Date(); + const markAsRead = () => { + alert("Marked as read."); + }; + const { i18n } = useLingui(); + + return ( +
+ +

Message Inbox

+

+ + See all unread messages + {" or "} + mark them as read. + +

+

+ +

+ < footer > Last login on {i18n.date(lastLogin)}. +
+ ); +} \ No newline at end of file diff --git a/examples/rspack/src/LocaleSwitcher.tsx b/examples/rspack/src/LocaleSwitcher.tsx new file mode 100644 index 000000000..a7781ff96 --- /dev/null +++ b/examples/rspack/src/LocaleSwitcher.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useLingui } from '@lingui/react'; +import Locale from './locales'; + +function LocaleSwitcher() { + const { i18n } = useLingui(); + + const handleLocaleChange = (newLocale: Locale) => { + i18n.activate(newLocale); + }; + + return ( +
+ + + {/* Add more buttons for other supported locales */} +
+ ); +} + +export default LocaleSwitcher; diff --git a/examples/rspack/src/assets/react.svg b/examples/rspack/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/examples/rspack/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/rspack/src/index.css b/examples/rspack/src/index.css new file mode 100644 index 000000000..917888c1d --- /dev/null +++ b/examples/rspack/src/index.css @@ -0,0 +1,70 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/rspack/src/locales.ts b/examples/rspack/src/locales.ts new file mode 100644 index 000000000..fbebdae9d --- /dev/null +++ b/examples/rspack/src/locales.ts @@ -0,0 +1,6 @@ +enum Locale { + ENGLISH = 'en', + FRENCH = 'fr', +} + +export default Locale; diff --git a/examples/rspack/src/locales/en/messages.d.ts b/examples/rspack/src/locales/en/messages.d.ts new file mode 100644 index 000000000..1c1427cb3 --- /dev/null +++ b/examples/rspack/src/locales/en/messages.d.ts @@ -0,0 +1,4 @@ +import { Messages } from '@lingui/core'; + declare const messages: Messages; + export { messages }; + \ No newline at end of file diff --git a/examples/rspack/src/locales/en/messages.po b/examples/rspack/src/locales/en/messages.po new file mode 100644 index 000000000..741374172 --- /dev/null +++ b/examples/rspack/src/locales/en/messages.po @@ -0,0 +1,30 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2023-08-23 14:57-0400\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: en\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Plural-Forms: \n" + +#: src/Inbox.tsx:30 +msgid "{messagesCount, plural, one {There's # message in your inbox.} other {There are # messages in your inbox.}}" +msgstr "{messagesCount, plural, one {There's # message in your inbox.} other {There are # messages in your inbox.}}" + +#: src/Inbox.tsx:32 +msgid "Last login on {0}." +msgstr "Last login on {0}." + +#: src/Inbox.tsx:21 +msgid "Message Inbox" +msgstr "Message Inbox" + +#: src/Inbox.tsx:23 +msgid "See all <0>unread messages or <1>mark them as read." +msgstr "See all <0>unread messages or <1>mark them as read." diff --git a/examples/rspack/src/locales/en/messages.ts b/examples/rspack/src/locales/en/messages.ts new file mode 100644 index 000000000..c435a0e70 --- /dev/null +++ b/examples/rspack/src/locales/en/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/export const messages=JSON.parse("{\"iHvPFN\":[[\"messagesCount\",\"plural\",{\"one\":[\"There's \",\"#\",\" message in your inbox.\"],\"other\":[\"There are \",\"#\",\" messages in your inbox.\"]}]],\"ItXLVU\":[\"Last login on \",[\"0\"],\".\"],\"8bWV5m\":\"Message Inbox\",\"f8BUjV\":\"See all <0>unread messages or <1>mark them as read.\"}"); \ No newline at end of file diff --git a/examples/rspack/src/locales/fr/messages.d.ts b/examples/rspack/src/locales/fr/messages.d.ts new file mode 100644 index 000000000..1c1427cb3 --- /dev/null +++ b/examples/rspack/src/locales/fr/messages.d.ts @@ -0,0 +1,4 @@ +import { Messages } from '@lingui/core'; + declare const messages: Messages; + export { messages }; + \ No newline at end of file diff --git a/examples/rspack/src/locales/fr/messages.po b/examples/rspack/src/locales/fr/messages.po new file mode 100644 index 000000000..fad0af5cf --- /dev/null +++ b/examples/rspack/src/locales/fr/messages.po @@ -0,0 +1,30 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2023-08-23 14:57-0400\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: @lingui/cli\n" +"Language: fr\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Plural-Forms: \n" + +#: src/Inbox.tsx:30 +msgid "{messagesCount, plural, one {There's # message in your inbox.} other {There are # messages in your inbox.}}" +msgstr "{messagesCount, plural, one {Il y a # message dans boîte de réception.} other {Il y a # messages dans votre boîte de réception.}}" + +#: src/Inbox.tsx:32 +msgid "Last login on {0}." +msgstr "Dernière connexion le {0}." + +#: src/Inbox.tsx:21 +msgid "Message Inbox" +msgstr "Boîte de réception de messages." + +#: src/Inbox.tsx:23 +msgid "See all <0>unread messages or <1>mark them as read." +msgstr "Voir tous <0>les messages non lus ou <1>les marquer comme lus." diff --git a/examples/rspack/src/locales/fr/messages.ts b/examples/rspack/src/locales/fr/messages.ts new file mode 100644 index 000000000..79913fe19 --- /dev/null +++ b/examples/rspack/src/locales/fr/messages.ts @@ -0,0 +1 @@ +/*eslint-disable*/export const messages=JSON.parse("{\"iHvPFN\":[[\"messagesCount\",\"plural\",{\"one\":[\"Il y a \",\"#\",\" message dans boîte de réception.\"],\"other\":[\"Il y a \",\"#\",\" messages dans votre boîte de réception.\"]}]],\"ItXLVU\":[\"Last login on \",[\"0\"],\".\"],\"8bWV5m\":\"Boîte de réception de messages.\",\"f8BUjV\":\"Voir tous <0>les messages non lus ou <1>les marquer comme lus.\"}"); \ No newline at end of file diff --git a/examples/rspack/src/main.tsx b/examples/rspack/src/main.tsx new file mode 100644 index 000000000..618653b12 --- /dev/null +++ b/examples/rspack/src/main.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import { i18n } from "@lingui/core"; +import { I18nProvider } from "@lingui/react"; +import { messages as enMessages } from "./locales/en/messages"; +import { messages as frMessages } from "./locales/fr/messages"; + +import Inbox from "./Inbox"; + +i18n.load({ + "en": enMessages, + "fr": frMessages, +}); + +i18n.activate("en"); + +const App = () => ( + + + +); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render( + + + +) diff --git a/examples/rspack/src/react-env.d.ts b/examples/rspack/src/react-env.d.ts new file mode 100644 index 000000000..ef67cc72c --- /dev/null +++ b/examples/rspack/src/react-env.d.ts @@ -0,0 +1,218 @@ +// CSS modules +type CSSModuleClasses = { readonly [key: string]: string }; + +declare module "*.module.css" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.scss" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.sass" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.less" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.styl" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.stylus" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.pcss" { + const classes: CSSModuleClasses; + export default classes; +} +declare module "*.module.sss" { + const classes: CSSModuleClasses; + export default classes; +} + +// CSS +declare module "*.css" { + /** + * @deprecated Use `import style from './style.css?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.scss" { + /** + * @deprecated Use `import style from './style.scss?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.sass" { + /** + * @deprecated Use `import style from './style.sass?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.less" { + /** + * @deprecated Use `import style from './style.less?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.styl" { + /** + * @deprecated Use `import style from './style.styl?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.stylus" { + /** + * @deprecated Use `import style from './style.stylus?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.pcss" { + /** + * @deprecated Use `import style from './style.pcss?inline'` instead. + */ + const css: string; + export default css; +} +declare module "*.sss" { + /** + * @deprecated Use `import style from './style.sss?inline'` instead. + */ + const css: string; + export default css; +} + +// images +declare module "*.png" { + const src: string; + export default src; +} +declare module "*.jpg" { + const src: string; + export default src; +} +declare module "*.jpeg" { + const src: string; + export default src; +} +declare module "*.jfif" { + const src: string; + export default src; +} +declare module "*.pjpeg" { + const src: string; + export default src; +} +declare module "*.pjp" { + const src: string; + export default src; +} +declare module "*.gif" { + const src: string; + export default src; +} +declare module "*.svg" { + const ReactComponent: React.FC>; + const content: string; + + // export { ReactComponent }; + export default content; +} +declare module "*.ico" { + const src: string; + export default src; +} +declare module "*.webp" { + const src: string; + export default src; +} +declare module "*.avif" { + const src: string; + export default src; +} + +// media +declare module "*.mp4" { + const src: string; + export default src; +} +declare module "*.webm" { + const src: string; + export default src; +} +declare module "*.ogg" { + const src: string; + export default src; +} +declare module "*.mp3" { + const src: string; + export default src; +} +declare module "*.wav" { + const src: string; + export default src; +} +declare module "*.flac" { + const src: string; + export default src; +} +declare module "*.aac" { + const src: string; + export default src; +} + +declare module "*.opus" { + const src: string; + export default src; +} + +// fonts +declare module "*.woff" { + const src: string; + export default src; +} +declare module "*.woff2" { + const src: string; + export default src; +} +declare module "*.eot" { + const src: string; + export default src; +} +declare module "*.ttf" { + const src: string; + export default src; +} +declare module "*.otf" { + const src: string; + export default src; +} + +// other +declare module "*.webmanifest" { + const src: string; + export default src; +} +declare module "*.pdf" { + const src: string; + export default src; +} +declare module "*.txt" { + const src: string; + export default src; +} + +declare module "*.po" { + const src: string; + export default src; +} diff --git a/examples/rspack/tsconfig.json b/examples/rspack/tsconfig.json new file mode 100644 index 000000000..036ec8286 --- /dev/null +++ b/examples/rspack/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES6", + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/website/docs/misc/examples.md b/website/docs/misc/examples.md index 7b222304d..b04ee7da6 100644 --- a/website/docs/misc/examples.md +++ b/website/docs/misc/examples.md @@ -11,3 +11,4 @@ Check out the example projects below: - [React Native](https://github.com/lingui/js-lingui/tree/main/examples/react-native) - [React with Vite and Babel](https://github.com/lingui/js-lingui/tree/main/examples/vite-project-react-babel) - [React with Vite and SWC](https://github.com/lingui/js-lingui/tree/main/examples/vite-project-react-swc) +- [Rspack with Babel](https://github.com/lingui/js-lingui/tree/main/examples/rspack)