Просмотр исходного кода

feat(dashboard): Set up i18n with lingui

Michael Bromley 11 месяцев назад
Родитель
Сommit
8aa7fd2e6c

Разница между файлами не показана из-за своего большого размера
+ 716 - 539
package-lock.json


+ 2 - 2
package.json

@@ -63,8 +63,8 @@
     "@nx/nx-linux-x64-gnu": "17.2.8",
     "@nx/nx-win32-x64-msvc": "17.2.8",
     "@rollup/rollup-linux-x64-gnu": "^4.13.0",
-    "@swc/core-linux-x64-gnu": "1.4.7",
-    "@swc/core-darwin-arm64": "1.7.19"
+    "@swc/core-darwin-arm64": "1.7.19",
+    "@swc/core-linux-x64-gnu": "1.4.7"
   },
   "workspaces": {
     "packages": [

+ 12 - 0
packages/dashboard/lingui.config.js

@@ -0,0 +1,12 @@
+import { defineConfig } from '@lingui/cli';
+
+export default defineConfig({
+    sourceLocale: 'en',
+    locales: ['de', 'en'],
+    catalogs: [
+        {
+            path: '<rootDir>/src/i18n/locales/{locale}',
+            include: ['src'],
+        },
+    ],
+});

+ 8 - 3
packages/dashboard/package.json

@@ -10,6 +10,8 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@lingui/core": "^5.2.0",
+    "@lingui/react": "^5.2.0",
     "@radix-ui/react-avatar": "^1.1.3",
     "@radix-ui/react-collapsible": "^1.1.3",
     "@radix-ui/react-dialog": "^1.1.6",
@@ -18,7 +20,7 @@
     "@radix-ui/react-separator": "^1.1.2",
     "@radix-ui/react-slot": "^1.1.2",
     "@radix-ui/react-tooltip": "^1.1.8",
-    "@tailwindcss/vite": "^4.0.6",
+    "@tailwindcss/vite": "^4.0.7",
     "@tanstack/react-query": "^5.66.7",
     "@tanstack/react-router": "^1.105.0",
     "@types/node": "^22.13.4",
@@ -35,11 +37,14 @@
   },
   "devDependencies": {
     "@eslint/js": "^9.19.0",
+    "@lingui/babel-plugin-lingui-macro": "^5.2.0",
+    "@lingui/cli": "^5.2.0",
+    "@lingui/vite-plugin": "^5.2.0",
     "@tanstack/eslint-plugin-query": "^5.66.1",
     "@tanstack/router-devtools": "^1.105.0",
     "@tanstack/router-plugin": "^1.105.0",
-    "@types/react": "^19.0.8",
-    "@types/react-dom": "^19.0.3",
+    "@types/react": "^19.0.10",
+    "@types/react-dom": "^19.0.4",
     "@vitejs/plugin-react": "^4.3.4",
     "eslint": "^9.19.0",
     "eslint-plugin-react": "^7.37.4",

+ 0 - 55
packages/dashboard/src/app/dashboard/page.tsx

@@ -1,55 +0,0 @@
-import { AppSidebar } from "@/components/app-sidebar"
-import {
-  Breadcrumb,
-  BreadcrumbItem,
-  BreadcrumbLink,
-  BreadcrumbList,
-  BreadcrumbPage,
-  BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb"
-import { Separator } from "@/components/ui/separator"
-import {
-  SidebarInset,
-  SidebarProvider,
-  SidebarTrigger,
-} from "@/components/ui/sidebar"
-
-export default function Page() {
-  return (
-    <SidebarProvider>
-      <AppSidebar />
-      <SidebarInset>
-        <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
-          <div className="flex items-center gap-2 px-4">
-            <SidebarTrigger className="-ml-1" />
-            <Separator
-              orientation="vertical"
-              className="mr-2 data-[orientation=vertical]:h-4"
-            />
-            <Breadcrumb>
-              <BreadcrumbList>
-                <BreadcrumbItem className="hidden md:block">
-                  <BreadcrumbLink href="#">
-                    Building Your Application
-                  </BreadcrumbLink>
-                </BreadcrumbItem>
-                <BreadcrumbSeparator className="hidden md:block" />
-                <BreadcrumbItem>
-                  <BreadcrumbPage>Data Fetching</BreadcrumbPage>
-                </BreadcrumbItem>
-              </BreadcrumbList>
-            </Breadcrumb>
-          </div>
-        </header>
-        <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
-          <div className="grid auto-rows-min gap-4 md:grid-cols-3">
-            <div className="bg-muted/50 aspect-video rounded-xl" />
-            <div className="bg-muted/50 aspect-video rounded-xl" />
-            <div className="bg-muted/50 aspect-video rounded-xl" />
-          </div>
-          <div className="bg-muted/50 min-h-[100vh] flex-1 rounded-xl md:min-h-min" />
-        </div>
-      </SidebarInset>
-    </SidebarProvider>
-  )
-}

+ 0 - 11
packages/dashboard/src/app/login/page.tsx

@@ -1,11 +0,0 @@
-import { LoginForm } from "@/components/login-form"
-
-export default function LoginPage() {
-  return (
-    <div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
-      <div className="w-full max-w-sm md:max-w-3xl">
-        <LoginForm />
-      </div>
-    </div>
-  )
-}

+ 10 - 3
packages/dashboard/src/components/login-form.tsx

@@ -1,4 +1,5 @@
 import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert.js';
+import { Trans } from '@lingui/react/macro';
 import { cn } from '@/lib/utils';
 import { Button } from '@/components/ui/button';
 import { Card, CardContent } from '@/components/ui/card';
@@ -31,13 +32,17 @@ export function LoginForm({ className, onFormSubmit, isVerifying, loginError, ..
                     <form className="p-6 md:p-8" onSubmit={handleSubmit}>
                         <div className="flex flex-col gap-6">
                             <div className="flex flex-col items-center text-center">
-                                <h1 className="text-2xl font-bold">Welcome back</h1>
+                                <h1 className="text-2xl font-bold">
+                                    <Trans>Welcome back!</Trans>
+                                </h1>
                                 <p className="text-muted-foreground text-balance">
                                     Login to your Acme Inc account
                                 </p>
                             </div>
                             <div className="grid gap-3">
-                                <Label htmlFor="email">User</Label>
+                                <Label htmlFor="email">
+                                    <Trans>User</Trans>
+                                </Label>
                                 <Input
                                     name="username"
                                     id="username"
@@ -48,7 +53,9 @@ export function LoginForm({ className, onFormSubmit, isVerifying, loginError, ..
                             </div>
                             <div className="grid gap-3">
                                 <div className="flex items-center">
-                                    <Label htmlFor="password">Password</Label>
+                                    <Label htmlFor="password">
+                                        <Trans>Password</Trans>
+                                    </Label>
                                     <a
                                         href="#"
                                         className="ml-auto text-sm underline-offset-2 hover:underline"

+ 24 - 0
packages/dashboard/src/i18n/i18n-provider.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { i18n } from '@lingui/core';
+import { I18nProvider as LinguiI18nProvider } from '@lingui/react';
+
+export const locales = {
+    en: 'English',
+    de: 'Deutsch',
+};
+export const defaultLocale = 'en';
+
+/**
+ * We do a dynamic import of just the catalog that we need
+ * @param locale any locale string
+ */
+export async function dynamicActivate(locale: string, onActivate?: () => void) {
+    const { messages } = await import(`./locales/${locale}.po`);
+    i18n.load(locale, messages);
+    i18n.activate(locale);
+    onActivate?.();
+}
+
+export function I18nProvider({ children }: { children: React.ReactNode }) {
+    return <LinguiI18nProvider i18n={i18n}>{children}</LinguiI18nProvider>;
+}

+ 25 - 0
packages/dashboard/src/i18n/locales/de.po

@@ -0,0 +1,25 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2025-02-20 14:18+0100\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: de\n"
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Plural-Forms: \n"
+
+#: src/components/login-form.tsx:57
+msgid "Password"
+msgstr "Passwort"
+
+#: src/components/login-form.tsx:44
+msgid "User"
+msgstr "Benutzer"
+
+msgid "Welcome back!"
+msgstr "Willkommen zurück!"

+ 26 - 0
packages/dashboard/src/i18n/locales/en.po

@@ -0,0 +1,26 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2025-02-20 14:18+0100\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/components/login-form.tsx:57
+msgid "Password"
+msgstr "Password"
+
+#: src/components/login-form.tsx:44
+msgid "User"
+msgstr "User"
+
+#: src/components/login-form.tsx:36
+msgid "Welcome back!"
+msgstr "Welcome back!"

+ 18 - 6
packages/dashboard/src/main.tsx

@@ -1,6 +1,7 @@
 import { AuthProvider, useAuth } from '@/auth.js';
+import { defaultLocale, dynamicActivate, I18nProvider } from '@/i18n/i18n-provider.js';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import React from 'react';
+import React, { useEffect } from 'react';
 import ReactDOM from 'react-dom/client';
 import { RouterProvider, createRouter } from '@tanstack/react-router';
 
@@ -32,12 +33,23 @@ function InnerApp() {
 }
 
 function App() {
+    const [i18nLoaded, setI18nLoaded] = React.useState(false);
+    useEffect(() => {
+        // With this method we dynamically load the catalogs
+        dynamicActivate(defaultLocale, () => {
+            setI18nLoaded(true);
+        });
+    }, []);
     return (
-        <QueryClientProvider client={queryClient}>
-            <AuthProvider>
-                <InnerApp />
-            </AuthProvider>
-        </QueryClientProvider>
+        i18nLoaded && (
+            <I18nProvider>
+                <QueryClientProvider client={queryClient}>
+                    <AuthProvider>
+                        <InnerApp />
+                    </AuthProvider>
+                </QueryClientProvider>
+            </I18nProvider>
+        )
     );
 }
 

+ 12 - 1
packages/dashboard/vite.config.js

@@ -1,12 +1,23 @@
 import { defineConfig } from 'vite';
 import path from 'path';
 import react from '@vitejs/plugin-react';
+import babel from 'vite-plugin-babel';
+import { lingui } from '@lingui/vite-plugin';
 import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
 import tailwindcss from '@tailwindcss/vite';
 
 // https://vite.dev/config/
 export default defineConfig({
-    plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react(), tailwindcss()],
+    plugins: [
+        TanStackRouterVite({ autoCodeSplitting: true }),
+        react({
+            babel: {
+                plugins: ['@lingui/babel-plugin-lingui-macro'],
+            },
+        }),
+        lingui(),
+        tailwindcss(),
+    ],
     resolve: {
         alias: {
             '@': path.resolve(__dirname, './src'),

Некоторые файлы не были показаны из-за большого количества измененных файлов