From 85b75e9b330e22dbf36d06aa6019ddd7652462f9 Mon Sep 17 00:00:00 2001 From: Samuel Stroschein <35429197+samuelstroschein@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:22:05 -0800 Subject: [PATCH] add paraglide add commands --- .../react-cra/add-ons/paraglide/README.md | 7 +++ .../add-ons/paraglide/assets/messages/de.json | 9 ++++ .../add-ons/paraglide/assets/messages/en.json | 9 ++++ .../assets/project.inlang/.gitignore | 1 + .../assets/project.inlang/settings.json | 12 +++++ .../src/components/LocaleSwitcher.tsx.ejs | 44 +++++++++++++++++++ .../assets/src/routes/demo.i18n.tsx.ejs | 34 ++++++++++++++ .../react-cra/add-ons/paraglide/info.json | 25 +++++++++++ .../react-cra/add-ons/paraglide/package.json | 5 +++ .../add-ons/paraglide/small-logo.svg | 4 ++ .../add-ons/start/assets/src/router.tsx.ejs | 16 ++++++- .../add-ons/start/assets/src/server.ts.ejs | 9 ++++ .../add-ons/start/assets/vite.config.ts.ejs | 9 +++- .../react-cra/project/base/src/main.tsx.ejs | 11 ++++- .../project/base/src/routes/__root.tsx.ejs | 25 ++++++++++- .../react-cra/project/base/tsconfig.json.ejs | 3 +- .../react-cra/project/base/vite.config.ts.ejs | 9 +++- frameworks/react-cra/src/checksum.ts | 2 +- 18 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 frameworks/react-cra/add-ons/paraglide/README.md create mode 100644 frameworks/react-cra/add-ons/paraglide/assets/messages/de.json create mode 100644 frameworks/react-cra/add-ons/paraglide/assets/messages/en.json create mode 100644 frameworks/react-cra/add-ons/paraglide/assets/project.inlang/.gitignore create mode 100644 frameworks/react-cra/add-ons/paraglide/assets/project.inlang/settings.json create mode 100644 frameworks/react-cra/add-ons/paraglide/assets/src/components/LocaleSwitcher.tsx.ejs create mode 100644 frameworks/react-cra/add-ons/paraglide/assets/src/routes/demo.i18n.tsx.ejs create mode 100644 frameworks/react-cra/add-ons/paraglide/info.json create mode 100644 frameworks/react-cra/add-ons/paraglide/package.json create mode 100644 frameworks/react-cra/add-ons/paraglide/small-logo.svg create mode 100644 frameworks/react-cra/add-ons/start/assets/src/server.ts.ejs diff --git a/frameworks/react-cra/add-ons/paraglide/README.md b/frameworks/react-cra/add-ons/paraglide/README.md new file mode 100644 index 00000000..e8baf23f --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/README.md @@ -0,0 +1,7 @@ +# Paraglide i18n + +This add-on wires up ParaglideJS for localized routing and message formatting. + +- Messages live in `project.inlang/messages`. +- URLs are localized through the Paraglide Vite plugin and router `rewrite` hooks. +- Run the dev server or build to regenerate the `src/paraglide` outputs. diff --git a/frameworks/react-cra/add-ons/paraglide/assets/messages/de.json b/frameworks/react-cra/add-ons/paraglide/assets/messages/de.json new file mode 100644 index 00000000..a44f188a --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/assets/messages/de.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "home_page": "Startseite", + "about_page": "Über uns", + "example_message": "Willkommen in deiner i18n-App.", + "language_label": "Sprache", + "current_locale": "Aktuelle Sprache: {locale}", + "learn_router": "Paraglide JS lernen" +} diff --git a/frameworks/react-cra/add-ons/paraglide/assets/messages/en.json b/frameworks/react-cra/add-ons/paraglide/assets/messages/en.json new file mode 100644 index 00000000..80f9e247 --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/assets/messages/en.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "home_page": "Home page", + "about_page": "About page", + "example_message": "Welcome to your i18n app.", + "language_label": "Language", + "current_locale": "Current locale: {locale}", + "learn_router": "Learn Paraglide JS" +} diff --git a/frameworks/react-cra/add-ons/paraglide/assets/project.inlang/.gitignore b/frameworks/react-cra/add-ons/paraglide/assets/project.inlang/.gitignore new file mode 100644 index 00000000..06cf6539 --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/assets/project.inlang/.gitignore @@ -0,0 +1 @@ +cache diff --git a/frameworks/react-cra/add-ons/paraglide/assets/project.inlang/settings.json b/frameworks/react-cra/add-ons/paraglide/assets/project.inlang/settings.json new file mode 100644 index 00000000..9bdce4c8 --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/assets/project.inlang/settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en", "de"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/frameworks/react-cra/add-ons/paraglide/assets/src/components/LocaleSwitcher.tsx.ejs b/frameworks/react-cra/add-ons/paraglide/assets/src/components/LocaleSwitcher.tsx.ejs new file mode 100644 index 00000000..370d89b8 --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/assets/src/components/LocaleSwitcher.tsx.ejs @@ -0,0 +1,44 @@ +// Locale switcher refs: +// - Paraglide docs: https://inlang.com/m/gerre34r/library-inlang-paraglideJs +// - Router example: https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#switching-locale +import { getLocale, locales, setLocale } from '@/paraglide/runtime' +import { m } from '@/paraglide/messages' + +export default function ParaglideLocaleSwitcher() { + const currentLocale = getLocale() + + return ( +
+ {m.current_locale({ locale: currentLocale })} +
+ {locales.map((locale) => ( + + ))} +
+
+ ) +} diff --git a/frameworks/react-cra/add-ons/paraglide/assets/src/routes/demo.i18n.tsx.ejs b/frameworks/react-cra/add-ons/paraglide/assets/src/routes/demo.i18n.tsx.ejs new file mode 100644 index 00000000..6f6200ae --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/assets/src/routes/demo.i18n.tsx.ejs @@ -0,0 +1,34 @@ +import { createFileRoute } from "@tanstack/react-router"; +import logo from "../logo.svg"; +import { m } from "@/paraglide/messages"; +import LocaleSwitcher from "../components/LocaleSwitcher"; + +export const Route = createFileRoute("/demo/i18n")({ + component: App, +}); + +function App() { + return ( +
+
+ logo +

{m.example_message({ username: "TanStack Router" })}

+ + {m.learn_router()} + +
+ +
+
+
+ ); +} diff --git a/frameworks/react-cra/add-ons/paraglide/info.json b/frameworks/react-cra/add-ons/paraglide/info.json new file mode 100644 index 00000000..6bffd29a --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/info.json @@ -0,0 +1,25 @@ +{ + "name": "Paraglide (i18n)", + "description": "i18n with localized routing", + "phase": "add-on", + "modes": ["file-router"], + "type": "add-on", + "priority": 30, + "link": "https://github.com/paraglidejs/paraglide-js", + "routes": [ + { + "icon": "Languages", + "url": "/demo/i18n", + "name": "I18n example", + "path": "src/routes/demo.i18n.tsx", + "jsName": "I18nDemo" + } + ], + "integrations": [ + { + "type": "header-user", + "path": "src/components/LocaleSwitcher.tsx", + "jsName": "ParaglideLocaleSwitcher" + } + ] +} diff --git a/frameworks/react-cra/add-ons/paraglide/package.json b/frameworks/react-cra/add-ons/paraglide/package.json new file mode 100644 index 00000000..77ba5d6d --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@inlang/paraglide-js": "^2.6.0" + } +} diff --git a/frameworks/react-cra/add-ons/paraglide/small-logo.svg b/frameworks/react-cra/add-ons/paraglide/small-logo.svg new file mode 100644 index 00000000..abf260ac --- /dev/null +++ b/frameworks/react-cra/add-ons/paraglide/small-logo.svg @@ -0,0 +1,4 @@ + + + 🪂 + diff --git a/frameworks/react-cra/add-ons/start/assets/src/router.tsx.ejs b/frameworks/react-cra/add-ons/start/assets/src/router.tsx.ejs index 2d458778..9aa0235e 100644 --- a/frameworks/react-cra/add-ons/start/assets/src/router.tsx.ejs +++ b/frameworks/react-cra/add-ons/start/assets/src/router.tsx.ejs @@ -4,6 +4,8 @@ import * as TanstackQuery from "./integrations/tanstack-query/root-provider"; <% } %> <% if (addOnEnabled.sentry) { %> import * as Sentry from "@sentry/tanstackstart-react"; +<% } %><% if (addOnEnabled.paraglide) { %> +import { deLocalizeUrl, localizeUrl } from "./paraglide/runtime"; <% } %> // Import the generated route tree @@ -16,7 +18,12 @@ export const getRouter = () => { const router = createRouter({ routeTree, - context: { ...rqContext }, + context: { ...rqContext },<% if (addOnEnabled.paraglide) { %> + // Paraglide URL rewrite docs: https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#rewrite-url + rewrite: { + input: ({ url }) => deLocalizeUrl(url), + output: ({ url }) => localizeUrl(url), + },<% } %> defaultPreload: "intent", Wrap: (props: { children: React.ReactNode }) => { return ( @@ -30,7 +37,12 @@ export const getRouter = () => { setupRouterSsrQueryIntegration({router, queryClient: rqContext.queryClient}) <% } else { %> const router = createRouter({ - routeTree, + routeTree,<% if (addOnEnabled.paraglide) { %> + // Paraglide URL rewrite docs: https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#rewrite-url + rewrite: { + input: ({ url }) => deLocalizeUrl(url), + output: ({ url }) => localizeUrl(url), + },<% } %> scrollRestoration: true, defaultPreloadStaleTime: 0, }) diff --git a/frameworks/react-cra/add-ons/start/assets/src/server.ts.ejs b/frameworks/react-cra/add-ons/start/assets/src/server.ts.ejs new file mode 100644 index 00000000..72ddbae5 --- /dev/null +++ b/frameworks/react-cra/add-ons/start/assets/src/server.ts.ejs @@ -0,0 +1,9 @@ +<% if (!addOnEnabled.paraglide) { ignoreFile() } %>import { paraglideMiddleware } from './paraglide/server' +import handler from '@tanstack/react-start/server-entry' + +// Server-side URL localization/redirects for Paraglide +export default { + fetch(req: Request): Promise { + return paraglideMiddleware(req, ({ request }) => handler.fetch(request)) + }, +} diff --git a/frameworks/react-cra/add-ons/start/assets/vite.config.ts.ejs b/frameworks/react-cra/add-ons/start/assets/vite.config.ts.ejs index cc31cb71..1d7e84e1 100644 --- a/frameworks/react-cra/add-ons/start/assets/vite.config.ts.ejs +++ b/frameworks/react-cra/add-ons/start/assets/vite.config.ts.ejs @@ -1,5 +1,8 @@ import { defineConfig } from 'vite' import { devtools } from '@tanstack/devtools-vite' +<% if (addOnEnabled.paraglide) { -%> +import { paraglideVitePlugin } from "@inlang/paraglide-js" +<% } -%> import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import viteReact from '@vitejs/plugin-react' import viteTsConfigPaths from 'vite-tsconfig-paths'<% if (tailwind) { %> @@ -8,7 +11,11 @@ import tailwindcss from "@tailwindcss/vite" <% } %> const config = defineConfig({ - plugins: [devtools(), <% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> + plugins: [devtools(), <% if (addOnEnabled.paraglide) { %>paraglideVitePlugin({ + project: './project.inlang', + outdir: './src/paraglide', + strategy: ['url'], + }), <% } %><% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> // this is the plugin that enables path aliases viteTsConfigPaths({ projects: ['./tsconfig.json'], diff --git a/frameworks/react-cra/project/base/src/main.tsx.ejs b/frameworks/react-cra/project/base/src/main.tsx.ejs index 6f09da37..cee4d997 100644 --- a/frameworks/react-cra/project/base/src/main.tsx.ejs +++ b/frameworks/react-cra/project/base/src/main.tsx.ejs @@ -105,6 +105,8 @@ import ReactDOM from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; <% for(const integration of integrations.filter(i => i.type === 'root-provider')) { %> import * as <%= integration.jsName %> from "<%= relativePath(integration.path) %>"; +<% } %><% if (addOnEnabled.paraglide) { %> +import { deLocalizeUrl, localizeUrl } from "./paraglide/runtime"; <% } %> // Import the generated route tree @@ -122,7 +124,12 @@ const <%= integration.jsName %>Context = <%= integration.jsName %>.getContext(); <% for(const integration of integrations.filter(i => i.type === 'root-provider')) { %> ...<%= integration.jsName %>Context, <% } %> - }, + },<% if (addOnEnabled.paraglide) { %> + // Paraglide URL rewrite docs: https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#rewrite-url + rewrite: { + input: ({ url }) => deLocalizeUrl(url), + output: ({ url }) => localizeUrl(url), + },<% } %> defaultPreload: "intent", scrollRestoration: true, defaultStructuralSharing: true, @@ -156,4 +163,4 @@ if (rootElement && !rootElement.innerHTML) { // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals();<% } %> \ No newline at end of file +reportWebVitals();<% } %> diff --git a/frameworks/react-cra/project/base/src/routes/__root.tsx.ejs b/frameworks/react-cra/project/base/src/routes/__root.tsx.ejs index 97f5922d..3c453cb8 100644 --- a/frameworks/react-cra/project/base/src/routes/__root.tsx.ejs +++ b/frameworks/react-cra/project/base/src/routes/__root.tsx.ejs @@ -1,11 +1,13 @@ <% if (!fileRouter) { ignoreFile() } %>import { <% if (addOnEnabled.start) { %> - HeadContent<% } else { %>Outlet<% } %><% if (addOnEnabled.start) { %>, Scripts<% } %>, <% if (addOnEnabled["tanstack-query"]) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router' + HeadContent<% } else { %>Outlet<% } %><% if (addOnEnabled.start) { %>, Scripts<% } %>, <% if (addOnEnabled["tanstack-query"]) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %><% if (addOnEnabled.paraglide) { %>, redirect<% } %> } from '@tanstack/react-router' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'; import { TanStackDevtools } from '@tanstack/react-devtools' <% if (addOns.length) { %> import Header from '../components/Header' <% } %><% for(const integration of integrations.filter(i => i.type === 'layout' || i.type === 'provider' || i.type === 'devtools')) { %> import <%= integration.jsName %> from '<%= relativePath(integration.path, true) %>' +<% } %><% if (addOnEnabled.paraglide) { %> +import { getLocale, shouldRedirect } from '@/paraglide/runtime' <% } %> <% if (addOnEnabled.start) { %> import appCss from '../styles.css?url' @@ -24,6 +26,25 @@ interface MyRouterContext { }<% } %> export const Route = <% if (addOnEnabled["tanstack-query"]) { %>createRootRouteWithContext()<% } else { %>createRootRoute<% } %>({ +<% if (addOnEnabled.paraglide) { %> + beforeLoad: async () => { + // Other redirect strategies are possible; see + // https://github.com/TanStack/router/tree/main/examples/react/i18n-paraglide#offline-redirect + if (typeof document !== 'undefined') { + document.documentElement.setAttribute('lang', getLocale()) + } + + <% if (!addOnEnabled.start) { %>// Client-side fallback redirect for SPA/file-router builds. Start apps should + // prefer server-side paraglideMiddleware (see start template server.ts). + if (typeof window !== 'undefined') { + const decision = await shouldRedirect({ url: window.location.href }) + + if (decision.redirectUrl) { + throw redirect({ href: decision.redirectUrl.href }) + } + }<% } %> + }, +<% } %> <% if (addOnEnabled.start) { %> head: () => ({ meta: [ @@ -76,7 +97,7 @@ export const Route = <% if (addOnEnabled["tanstack-query"]) { %>createRootRouteW <% if (addOnEnabled.start) { %> function RootDocument({ children }: { children: React.ReactNode }) { return ( - + <% if (addOnEnabled.paraglide) { %><% } else { %><% } %> diff --git a/frameworks/react-cra/project/base/tsconfig.json.ejs b/frameworks/react-cra/project/base/tsconfig.json.ejs index 6f2c68f4..914216dd 100644 --- a/frameworks/react-cra/project/base/tsconfig.json.ejs +++ b/frameworks/react-cra/project/base/tsconfig.json.ejs @@ -10,7 +10,8 @@ /* Bundler mode */ "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "allowImportingTsExtensions": true,<% if (addOnEnabled.paraglide) { %> + "allowJs": true,<% } %> "verbatimModuleSyntax": <%= addOnEnabled['start'] ? 'false' : 'true' %>, "noEmit": true, diff --git a/frameworks/react-cra/project/base/vite.config.ts.ejs b/frameworks/react-cra/project/base/vite.config.ts.ejs index 734dddb3..38029856 100644 --- a/frameworks/react-cra/project/base/vite.config.ts.ejs +++ b/frameworks/react-cra/project/base/vite.config.ts.ejs @@ -1,5 +1,6 @@ <% if (addOnEnabled.start) { ignoreFile() } %>import { defineConfig } from "vite"; -import { devtools } from '@tanstack/devtools-vite' +import { devtools } from '@tanstack/devtools-vite'<% if (addOnEnabled.paraglide) { %> +import { paraglideVitePlugin } from "@inlang/paraglide-js"<% } %> import viteReact from "@vitejs/plugin-react";<% if (tailwind) { %> import tailwindcss from "@tailwindcss/vite"; <% } %><%if (fileRouter) { %> @@ -11,7 +12,11 @@ import federationConfig from "./module-federation.config.js";<% } %><% for(const // https://vitejs.dev/config/ export default defineConfig({ - plugins: [devtools(), <% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> <% if(fileRouter) { %>tanstackRouter({ + plugins: [devtools(), <% if (addOnEnabled.paraglide) { %>paraglideVitePlugin({ + project: './project.inlang', + outdir: './src/paraglide', + strategy: ['url'], + }), <% } %><% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %> <% if(fileRouter) { %>tanstackRouter({ target: "react", autoCodeSplitting: true, }), <% } %>viteReact(<% if (addOnEnabled.compiler) { %>{ diff --git a/frameworks/react-cra/src/checksum.ts b/frameworks/react-cra/src/checksum.ts index 2c8a71e5..4f7020d3 100644 --- a/frameworks/react-cra/src/checksum.ts +++ b/frameworks/react-cra/src/checksum.ts @@ -1,3 +1,3 @@ // This file is auto-generated. Do not edit manually. // Generated from add-ons, examples, hosts, project, and toolchains directories -export const contentChecksum = 'b722b3f8235cbf4618c508da1b499a8a6a583d8107eebeb9e2ffe19716ddc7e9' +export const contentChecksum = '59725486fa40de3b9dbd9b080b1f844dfd17bfdfd5fd79df23f296586ab8ee47'