Browse Source

feat: setup next-intl with initial page props

Nicolas Meienberger 2 years ago
parent
commit
b09c12eda1

+ 3 - 0
crowdin.yml

@@ -0,0 +1,3 @@
+files:
+  - source: /src/client/messages/en.json
+    translation: /src/client/messages/%locale%.json

+ 2 - 0
global.d.ts

@@ -0,0 +1,2 @@
+type Messages = typeof import('./src/client/messages/en.json');
+type IntlMessages = Messages;

+ 4 - 0
package.json

@@ -53,9 +53,12 @@
     "fs-extra": "^11.1.0",
     "isomorphic-fetch": "^3.0.0",
     "jsonwebtoken": "^9.0.0",
+    "lodash.merge": "^4.6.2",
     "next": "13.2.4",
+    "next-intl": "^2.13.1",
     "node-cron": "^3.0.1",
     "node-fetch-commonjs": "^3.2.4",
+    "nookies": "^2.5.2",
     "pg": "^8.10.0",
     "qrcode.react": "^3.1.0",
     "react": "18.2.0",
@@ -95,6 +98,7 @@
     "@types/isomorphic-fetch": "^0.0.36",
     "@types/jest": "^29.5.0",
     "@types/jsonwebtoken": "^9.0.0",
+    "@types/lodash.merge": "^4.6.7",
     "@types/node": "18.15.3",
     "@types/node-cron": "^3.0.2",
     "@types/pg": "^8.6.6",

+ 110 - 2
pnpm-lock.yaml

@@ -79,15 +79,24 @@ dependencies:
   jsonwebtoken:
     specifier: ^9.0.0
     version: 9.0.0
+  lodash.merge:
+    specifier: ^4.6.2
+    version: 4.6.2
   next:
     specifier: 13.2.4
     version: 13.2.4(@babel/core@7.21.3)(react-dom@18.2.0)(react@18.2.0)(sass@1.59.3)
+  next-intl:
+    specifier: ^2.13.1
+    version: 2.13.1(next@13.2.4)(react@18.2.0)
   node-cron:
     specifier: ^3.0.1
     version: 3.0.2
   node-fetch-commonjs:
     specifier: ^3.2.4
     version: 3.2.4
+  nookies:
+    specifier: ^2.5.2
+    version: 2.5.2
   pg:
     specifier: ^8.10.0
     version: 8.10.0
@@ -201,6 +210,9 @@ devDependencies:
   '@types/jsonwebtoken':
     specifier: ^9.0.0
     version: 9.0.1
+  '@types/lodash.merge':
+    specifier: ^4.6.7
+    version: 4.6.7
   '@types/node':
     specifier: 18.15.3
     version: 18.15.3
@@ -1077,6 +1089,53 @@ packages:
       - '@types/react'
     dev: false
 
+  /@formatjs/ecma402-abstract@1.11.4:
+    resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
+    dependencies:
+      '@formatjs/intl-localematcher': 0.2.25
+      tslib: 2.5.0
+    dev: false
+
+  /@formatjs/ecma402-abstract@1.15.0:
+    resolution: {integrity: sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==}
+    dependencies:
+      '@formatjs/intl-localematcher': 0.2.32
+      tslib: 2.5.0
+    dev: false
+
+  /@formatjs/fast-memoize@1.2.1:
+    resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
+    dependencies:
+      tslib: 2.5.0
+    dev: false
+
+  /@formatjs/icu-messageformat-parser@2.1.0:
+    resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==}
+    dependencies:
+      '@formatjs/ecma402-abstract': 1.11.4
+      '@formatjs/icu-skeleton-parser': 1.3.6
+      tslib: 2.5.0
+    dev: false
+
+  /@formatjs/icu-skeleton-parser@1.3.6:
+    resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==}
+    dependencies:
+      '@formatjs/ecma402-abstract': 1.11.4
+      tslib: 2.5.0
+    dev: false
+
+  /@formatjs/intl-localematcher@0.2.25:
+    resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==}
+    dependencies:
+      tslib: 2.5.0
+    dev: false
+
+  /@formatjs/intl-localematcher@0.2.32:
+    resolution: {integrity: sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==}
+    dependencies:
+      tslib: 2.5.0
+    dev: false
+
   /@hookform/resolvers@2.9.11(react-hook-form@7.43.7):
     resolution: {integrity: sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==}
     peerDependencies:
@@ -2500,6 +2559,16 @@ packages:
       '@types/node': 18.15.3
     dev: true
 
+  /@types/lodash.merge@4.6.7:
+    resolution: {integrity: sha512-OwxUJ9E50gw3LnAefSHJPHaBLGEKmQBQ7CZe/xflHkyy/wH2zVyEIAKReHvVrrn7zKdF58p16We9kMfh7v0RRQ==}
+    dependencies:
+      '@types/lodash': 4.14.194
+    dev: true
+
+  /@types/lodash@4.14.194:
+    resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
+    dev: true
+
   /@types/mdast@3.0.10:
     resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
     dependencies:
@@ -5714,6 +5783,15 @@ packages:
       side-channel: 1.0.4
     dev: true
 
+  /intl-messageformat@9.13.0:
+    resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
+    dependencies:
+      '@formatjs/ecma402-abstract': 1.11.4
+      '@formatjs/fast-memoize': 1.2.1
+      '@formatjs/icu-messageformat-parser': 2.1.0
+      tslib: 2.5.0
+    dev: false
+
   /invariant@2.2.4:
     resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
     dependencies:
@@ -6727,7 +6805,6 @@ packages:
 
   /lodash.merge@4.6.2:
     resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
-    dev: true
 
   /lodash.throttle@4.1.1:
     resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
@@ -7397,6 +7474,20 @@ packages:
     engines: {node: '>= 0.6'}
     dev: false
 
+  /next-intl@2.13.1(next@13.2.4)(react@18.2.0):
+    resolution: {integrity: sha512-3XUZ7c123QHgQGcz5UUkTtakJdLETBlcHcdHop43iVToOpsezxvMZW6jxWwuHTRvkElfNPy1fhHwzBo/mhVVvQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@formatjs/intl-localematcher': 0.2.32
+      negotiator: 0.6.3
+      next: 13.2.4(@babel/core@7.21.3)(react-dom@18.2.0)(react@18.2.0)(sass@1.59.3)
+      react: 18.2.0
+      use-intl: 2.13.1(react@18.2.0)
+    dev: false
+
   /next-router-mock@0.9.2(next@13.2.4)(react@18.2.0):
     resolution: {integrity: sha512-rh6Mq1xhZ4Y0y9Z3seHZ04k4dAKnAyRcis7q3ZUF+Xp0uBeNqPC8Ydw5DldYncN3o1sYBqRyz25F/v/kfcg0/Q==}
     peerDependencies:
@@ -7524,6 +7615,13 @@ packages:
       undefsafe: 2.0.5
     dev: true
 
+  /nookies@2.5.2:
+    resolution: {integrity: sha512-x0TRSaosAEonNKyCrShoUaJ5rrT5KHRNZ5DwPCuizjgrnkpE5DRf3VL7AyyQin4htict92X1EQ7ejDbaHDVdYA==}
+    dependencies:
+      cookie: 0.4.2
+      set-cookie-parser: 2.5.1
+    dev: false
+
   /nopt@1.0.10:
     resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==}
     hasBin: true
@@ -8533,7 +8631,6 @@ packages:
 
   /set-cookie-parser@2.5.1:
     resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==}
-    dev: true
 
   /setprototypeof@1.2.0:
     resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
@@ -9316,6 +9413,17 @@ packages:
       tslib: 2.5.0
     dev: false
 
+  /use-intl@2.13.1(react@18.2.0):
+    resolution: {integrity: sha512-za8vb9UtKyFuDWbc+Iceqnz1KOAGwm9cTaBjW5af6e7ZcAdwADUwsz9M/8M9VDl5gKKQ/o+3TJcKdi+ieOKhfQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      '@formatjs/ecma402-abstract': 1.15.0
+      intl-messageformat: 9.13.0
+      react: 18.2.0
+    dev: false
+
   /use-isomorphic-layout-effect@1.1.2(@types/react@18.0.28)(react@18.2.0):
     resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
     peerDependencies:

+ 2 - 0
src/@types/next.d.ts

@@ -3,9 +3,11 @@ import { IncomingMessage } from 'http';
 import { Session } from 'express-session';
 import { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from 'next';
 import { ParsedUrlQuery } from 'querystring';
+import { Locale } from '@/shared/internationalization/locales';
 
 type SessionContent = {
   userId?: number;
+  locale?: Locale;
 };
 
 declare module 'express-session' {

+ 20 - 1
src/client/utils/page-helpers.ts

@@ -1,4 +1,6 @@
+import nookies from 'nookies';
 import { GetServerSideProps } from 'next';
+import { getLocaleFromString } from '@/shared/internationalization/locales';
 
 export const getAuthedPageProps: GetServerSideProps = async (ctx) => {
   const { userId } = ctx.req.session;
@@ -12,5 +14,22 @@ export const getAuthedPageProps: GetServerSideProps = async (ctx) => {
     };
   }
 
-  return { props: {} };
+  return {
+    props: {},
+  };
+};
+
+export const getMessagesPageProps: GetServerSideProps = async (ctx) => {
+  const cookies = nookies.get(ctx);
+  const { locale: sessionLocale } = ctx.req.session;
+  const { locale: cookieLocale } = cookies;
+  const browserLocale = ctx.req.headers['accept-language']?.split(',')[0];
+
+  const locale = sessionLocale || cookieLocale || browserLocale || 'en';
+
+  return {
+    props: {
+      messages: (await import(`../messages/${getLocaleFromString(locale)}.json`)).default,
+    },
+  };
 };

+ 12 - 7
src/pages/_app.tsx

@@ -2,10 +2,12 @@ import React, { useEffect } from 'react';
 import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 import type { AppProps } from 'next/app';
 import Head from 'next/head';
+import { NextIntlProvider } from 'next-intl';
 import '../client/styles/global.css';
 import '../client/styles/global.scss';
 import 'react-tooltip/dist/react-tooltip.css';
 import { Toaster } from 'react-hot-toast';
+import { useLocale } from '@/client/hooks/useLocale';
 import { useUIStore } from '../client/state/uiStore';
 import { StatusProvider } from '../client/components/hoc/StatusProvider';
 import { trpc } from '../client/utils/trpc';
@@ -20,6 +22,7 @@ import { SystemStatus, useSystemStore } from '../client/state/systemStore';
 function MyApp({ Component, pageProps }: AppProps) {
   const { setDarkMode } = useUIStore();
   const { setStatus, setVersion, pollStatus } = useSystemStore();
+  const { locale } = useLocale();
 
   trpc.system.status.useQuery(undefined, { networkMode: 'online', refetchInterval: 2000, onSuccess: (d) => setStatus((d.status as SystemStatus) || 'RUNNING'), enabled: pollStatus });
   const version = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
@@ -46,13 +49,15 @@ function MyApp({ Component, pageProps }: AppProps) {
 
   return (
     <main className="h-100">
-      <Head>
-        <title>Tipi</title>
-      </Head>
-      <StatusProvider>
-        <Component {...pageProps} />
-      </StatusProvider>
-      <Toaster />
+      <NextIntlProvider locale={locale} messages={pageProps.messages}>
+        <Head>
+          <title>Tipi</title>
+        </Head>
+        <StatusProvider>
+          <Component {...pageProps} />
+        </StatusProvider>
+        <Toaster />
+      </NextIntlProvider>
       <ReactQueryDevtools />
     </main>
   );

+ 10 - 8
src/pages/app-store/[id].tsx

@@ -1,17 +1,19 @@
-import { Context } from '@/server/context';
-import { getAuthedPageProps } from '@/utils/page-helpers';
-import { GetServerSidePropsContext } from 'next';
+import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
+import { GetServerSideProps } from 'next';
 
 export { AppDetailsPage as default } from '../../client/modules/Apps/pages/AppDetailsPage';
 
-export const getServerSideProps = async (ctx: Context & GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
   const authedProps = await getAuthedPageProps(ctx);
+  const messagesProps = await getMessagesPageProps(ctx);
 
   const { id } = ctx.query;
   const appId = String(id);
 
-  return {
-    ...authedProps,
-    props: { appId },
-  };
+  return merge(authedProps, messagesProps, {
+    props: {
+      appId,
+    },
+  });
 };

+ 5 - 4
src/pages/app-store/index.tsx

@@ -1,13 +1,14 @@
-import { getAuthedPageProps } from '@/utils/page-helpers';
+import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
 import { GetServerSideProps } from 'next';
 
 export { AppStorePage as default } from '../../client/modules/AppStore/pages/AppStorePage';
 
 export const getServerSideProps: GetServerSideProps = async (ctx) => {
   const authedProps = await getAuthedPageProps(ctx);
+  const messagesProps = await getMessagesPageProps(ctx);
 
-  return {
-    ...authedProps,
+  return merge(authedProps, messagesProps, {
     props: {},
-  };
+  });
 };

+ 10 - 8
src/pages/apps/[id].tsx

@@ -1,17 +1,19 @@
-import { Context } from '@/server/context';
-import { getAuthedPageProps } from '@/utils/page-helpers';
-import { GetServerSidePropsContext } from 'next';
+import merge from 'lodash.merge';
+import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
+import { GetServerSideProps } from 'next';
 
 export { AppDetailsPage as default } from '../../client/modules/Apps/pages/AppDetailsPage';
 
-export const getServerSideProps = async (ctx: Context & GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
   const authedProps = await getAuthedPageProps(ctx);
+  const messagesProps = await getMessagesPageProps(ctx);
 
   const { id } = ctx.query;
   const appId = String(id);
 
-  return {
-    ...authedProps,
-    props: { appId },
-  };
+  return merge(authedProps, messagesProps, {
+    props: {
+      appId,
+    },
+  });
 };

+ 5 - 4
src/pages/apps/index.tsx

@@ -1,13 +1,14 @@
-import { getAuthedPageProps } from '@/utils/page-helpers';
+import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
 import { GetServerSideProps } from 'next';
 
 export { AppsPage as default } from '../../client/modules/Apps/pages/AppsPage';
 
 export const getServerSideProps: GetServerSideProps = async (ctx) => {
   const authedProps = await getAuthedPageProps(ctx);
+  const messagesProps = await getMessagesPageProps(ctx);
 
-  return {
-    ...authedProps,
+  return merge(authedProps, messagesProps, {
     props: {},
-  };
+  });
 };

+ 5 - 4
src/pages/index.tsx

@@ -1,13 +1,14 @@
-import { getAuthedPageProps } from '@/utils/page-helpers';
+import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
 import { GetServerSideProps } from 'next';
 
 export { DashboardPage as default } from '../client/modules/Dashboard/pages/DashboardPage';
 
 export const getServerSideProps: GetServerSideProps = async (ctx) => {
   const authedProps = await getAuthedPageProps(ctx);
+  const messagesProps = await getMessagesPageProps(ctx);
 
-  return {
-    ...authedProps,
+  return merge(authedProps, messagesProps, {
     props: {},
-  };
+  });
 };

+ 7 - 3
src/pages/login.tsx

@@ -1,9 +1,13 @@
+import { getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
 import { GetServerSideProps } from 'next';
 
 export { LoginPage as default } from '../client/modules/Auth/pages/LoginPage';
 
-export const getServerSideProps: GetServerSideProps = async () => {
-  return {
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+  const messagesProps = await getMessagesPageProps(ctx);
+
+  return merge(messagesProps, {
     props: {},
-  };
+  });
 };

+ 7 - 3
src/pages/register.tsx

@@ -1,9 +1,13 @@
+import { getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
 import { GetServerSideProps } from 'next';
 
 export { RegisterPage as default } from '../client/modules/Auth/pages/RegisterPage';
 
-export const getServerSideProps: GetServerSideProps = async () => {
-  return {
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+  const messagesProps = await getMessagesPageProps(ctx);
+
+  return merge(messagesProps, {
     props: {},
-  };
+  });
 };

+ 12 - 0
src/pages/reset-password.tsx

@@ -1 +1,13 @@
+import { getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
+import { GetServerSideProps } from 'next';
+
 export { ResetPasswordPage as default } from '../client/modules/Auth/pages/ResetPasswordPage';
+
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+  const messagesProps = await getMessagesPageProps(ctx);
+
+  return merge(messagesProps, {
+    props: {},
+  });
+};

+ 5 - 4
src/pages/settings.tsx

@@ -1,13 +1,14 @@
-import { getAuthedPageProps } from '@/utils/page-helpers';
+import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
+import merge from 'lodash.merge';
 import { GetServerSideProps } from 'next';
 
 export { SettingsPage as default } from '../client/modules/Settings/pages/SettingsPage';
 
 export const getServerSideProps: GetServerSideProps = async (ctx) => {
   const authedProps = await getAuthedPageProps(ctx);
+  const messagesProps = await getMessagesPageProps(ctx);
 
-  return {
-    ...authedProps,
+  return merge(authedProps, messagesProps, {
     props: {},
-  };
+  });
 };