ソースを参照

Merge branch 'support/apply-nextjs-2' into feat/integrate-implement-page-alert-component

yohei0125 3 年 前
コミット
40a4b4ac1d

+ 0 - 1
.github/workflows/ci-app.yml

@@ -7,7 +7,6 @@ on:
       - rc/**
       - rc/**
       - chore/**
       - chore/**
       - support/prepare-v**
       - support/prepare-v**
-      - support/apply-nextjs-2
     paths:
     paths:
       - .github/workflows/ci-app.yml
       - .github/workflows/ci-app.yml
       - .eslint*
       - .eslint*

+ 0 - 2
packages/app/public/static/locales/index.js

@@ -1,2 +0,0 @@
-// !!DO NOT EDIT/REMOVE THIS FILE!!
-// entry point for @alienfast/i18next-loader

+ 1 - 2
packages/app/src/client/services/ContextExtractor.tsx

@@ -17,7 +17,7 @@ import {
   useIsNotCreatable, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useIsNotCreatable, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
-  useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
+  useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader,
   useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useIsEmptyPage, useEmptyPageId, useGrowiVersion,
   useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useIsEmptyPage, useEmptyPageId, useGrowiVersion,
 } from '../../stores/context';
 } from '../../stores/context';
@@ -151,7 +151,6 @@ const ContextExtractorOnce: FC = () => {
   useCreator(creator);
   useCreator(creator);
   useRevisionAuthor(revisionAuthor);
   useRevisionAuthor(revisionAuthor);
   useTargetAndAncestors(targetAndAncestors);
   useTargetAndAncestors(targetAndAncestors);
-  useNotFoundTargetPathOrId(notFoundTargetPathOrId);
   useIsSearchPage(isSearchPage);
   useIsSearchPage(isSearchPage);
   useIsEmptyPage(isEmptyPage);
   useIsEmptyPage(isEmptyPage);
   useHasParent(hasParent);
   useHasParent(hasParent);

+ 33 - 29
packages/app/src/components/Sidebar.tsx

@@ -2,6 +2,8 @@ import React, {
   useCallback, useEffect, useRef, useState,
   useCallback, useEffect, useRef, useState,
 } from 'react';
 } from 'react';
 
 
+import dynamic from 'next/dynamic';
+
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import {
 import {
   useDrawerMode, useDrawerOpened,
   useDrawerMode, useDrawerOpened,
@@ -14,9 +16,8 @@ import {
 
 
 import DrawerToggler from './Navbar/DrawerToggler';
 import DrawerToggler from './Navbar/DrawerToggler';
 import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
 import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
-import SidebarContents from './Sidebar/SidebarContents';
 import { SidebarNav } from './Sidebar/SidebarNav';
 import { SidebarNav } from './Sidebar/SidebarNav';
-import { StickyStretchableScroller } from './StickyStretchableScroller';
+import { StickyStretchableScrollerProps } from './StickyStretchableScroller';
 
 
 import styles from './Sidebar.module.scss';
 import styles from './Sidebar.module.scss';
 
 
@@ -54,32 +55,35 @@ const GlobalNavigation = () => {
 
 
 };
 };
 
 
-// const SidebarContentsWrapper = () => {
-//   const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
-
-//   const calcViewHeight = useCallback(() => {
-//     const elem = document.querySelector('#grw-sidebar-contents-wrapper');
-//     return elem != null
-//       ? window.innerHeight - elem?.getBoundingClientRect().top
-//       : window.innerHeight;
-//   }, []);
-
-//   return (
-//     <>
-//       <div id="grw-sidebar-contents-wrapper" style={{ minHeight: '100%' }}>
-//         <StickyStretchableScroller
-//           simplebarRef={mutateSidebarScroller}
-//           stickyElemSelector=".grw-sidebar"
-//           calcViewHeight={calcViewHeight}
-//         >
-//           <SidebarContents />
-//         </StickyStretchableScroller>
-//       </div>
-
-//       <DrawerToggler iconClass="icon-arrow-left" />
-//     </>
-//   );
-// };
+const SidebarContentsWrapper = () => {
+  const StickyStretchableScroller = dynamic<StickyStretchableScrollerProps>(() => import('./StickyStretchableScroller')
+    .then(mod => mod.StickyStretchableScroller), { ssr: false });
+  const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
+  const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
+
+  const calcViewHeight = useCallback(() => {
+    const elem = document.querySelector('#grw-sidebar-contents-wrapper');
+    return elem != null
+      ? window.innerHeight - elem?.getBoundingClientRect().top
+      : window.innerHeight;
+  }, []);
+
+  return (
+    <>
+      <div id="grw-sidebar-contents-wrapper" style={{ minHeight: '100%' }}>
+        <StickyStretchableScroller
+          simplebarRef={mutateSidebarScroller}
+          stickyElemSelector=".grw-sidebar"
+          calcViewHeight={calcViewHeight}
+        >
+          <SidebarContents />
+        </StickyStretchableScroller>
+      </div>
+
+      <DrawerToggler iconClass="icon-arrow-left" />
+    </>
+  );
+};
 
 
 
 
 const Sidebar = (): JSX.Element => {
 const Sidebar = (): JSX.Element => {
@@ -313,7 +317,7 @@ const Sidebar = (): JSX.Element => {
                 >
                 >
                   <div className="grw-contextual-navigation-child">
                   <div className="grw-contextual-navigation-child">
                     <div role="group" data-testid="grw-contextual-navigation-sub" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
                     <div role="group" data-testid="grw-contextual-navigation-sub" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
-                      {/* <SidebarContentsWrapper></SidebarContentsWrapper> */}
+                      <SidebarContentsWrapper></SidebarContentsWrapper>
                     </div>
                     </div>
                   </div>
                   </div>
                 </div>
                 </div>

+ 2 - 3
packages/app/src/components/Sidebar/PageTree.tsx

@@ -3,7 +3,7 @@ import React, { FC, memo } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import {
 import {
-  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser, useNotFoundTargetPathOrId,
+  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 
@@ -17,10 +17,9 @@ const PageTree: FC = memo(() => {
   const { data: currentPath } = useCurrentPagePath();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
-  const { data: notFoundTargetPathOrId } = useNotFoundTargetPathOrId();
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
 
-  const targetPathOrId = targetId || notFoundTargetPathOrId;
+  const targetPathOrId = targetId || currentPath;
 
 
   if (migrationStatus == null) {
   if (migrationStatus == null) {
     return (
     return (

+ 4 - 3
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -5,14 +5,15 @@ import React, {
 import nodePath from 'path';
 import nodePath from 'path';
 
 
 import { pathUtils, pagePathUtils } from '@growi/core';
 import { pathUtils, pagePathUtils } from '@growi/core';
-import { useDrag, useDrop } from 'react-dnd';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useDrag, useDrop } from 'react-dnd';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
 
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
+import { Nullable } from '~/interfaces/common';
 import {
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
@@ -35,7 +36,7 @@ const logger = loggerFactory('growi:cli:Item');
 interface ItemProps {
 interface ItemProps {
   isEnableActions: boolean
   isEnableActions: boolean
   itemNode: ItemNode
   itemNode: ItemNode
-  targetPathOrId?: string
+  targetPathOrId?: Nullable<string>
   isOpen?: boolean
   isOpen?: boolean
   isEnabledAttachTitleHeader?: boolean
   isEnabledAttachTitleHeader?: boolean
   onRenamed?(): void
   onRenamed?(): void
@@ -44,7 +45,7 @@ interface ItemProps {
 }
 }
 
 
 // Utility to mark target
 // Utility to mark target
-const markTarget = (children: ItemNode[], targetPathOrId?: string): void => {
+const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): void => {
   if (targetPathOrId == null) {
   if (targetPathOrId == null) {
     return;
     return;
   }
   }

+ 2 - 2
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -5,8 +5,8 @@ import React, {
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
-
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
+import { Nullable } from '~/interfaces/common';
 import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
@@ -84,7 +84,7 @@ const isSecondStageRenderingCondition = (condition: RenderingCondition|SecondSta
 type ItemsTreeProps = {
 type ItemsTreeProps = {
   isEnableActions: boolean
   isEnableActions: boolean
   targetPath: string
   targetPath: string
-  targetPathOrId?: string
+  targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
   targetAndAncestorsData?: TargetAndAncestors
 }
 }
 
 

+ 3 - 6
packages/app/src/components/Sidebar/SidebarContents.tsx

@@ -4,13 +4,13 @@ import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 
 // import CustomSidebar from './CustomSidebar';
 // import CustomSidebar from './CustomSidebar';
-// import PageTree from './PageTree';
+import PageTree from './PageTree';
 import RecentChanges from './RecentChanges';
 import RecentChanges from './RecentChanges';
 import Tag from './Tag';
 import Tag from './Tag';
 
 
 const DummyComponent = (): JSX.Element => <></>; // Todo: remove this later when it is able to render other Contents.
 const DummyComponent = (): JSX.Element => <></>; // Todo: remove this later when it is able to render other Contents.
 
 
-const SidebarContents = (): JSX.Element => {
+export const SidebarContents = (): JSX.Element => {
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
   let Contents;
   let Contents;
@@ -26,8 +26,7 @@ const SidebarContents = (): JSX.Element => {
       Contents = Tag;
       Contents = Tag;
       break;
       break;
     default:
     default:
-      // Contents = PageTree;
-      Contents = DummyComponent;
+      Contents = PageTree;
   }
   }
 
 
   return (
   return (
@@ -35,5 +34,3 @@ const SidebarContents = (): JSX.Element => {
   );
   );
 
 
 };
 };
-
-export default SidebarContents;

+ 2 - 1
packages/app/src/interfaces/user.ts

@@ -1,6 +1,7 @@
 import { IAttachment } from './attachment';
 import { IAttachment } from './attachment';
 import { Ref } from './common';
 import { Ref } from './common';
 import { HasObjectId } from './has-object-id';
 import { HasObjectId } from './has-object-id';
+import { Lang } from './lang';
 
 
 export type IUser = {
 export type IUser = {
   name: string,
   name: string,
@@ -14,7 +15,7 @@ export type IUser = {
   admin: boolean,
   admin: boolean,
   apiToken?: string,
   apiToken?: string,
   isEmailPublished: boolean,
   isEmailPublished: boolean,
-  lang: string,
+  lang: Lang,
   slackMemberId?: string,
   slackMemberId?: string,
 }
 }
 
 

+ 4 - 2
packages/app/src/next-i18next.config.ts

@@ -5,11 +5,13 @@ import I18nextChainedBackend from 'i18next-chained-backend';
 import I18NextHttpBackend from 'i18next-http-backend';
 import I18NextHttpBackend from 'i18next-http-backend';
 import I18NextLocalStorageBackend from 'i18next-localstorage-backend';
 import I18NextLocalStorageBackend from 'i18next-localstorage-backend';
 
 
+import { AllLang, Lang } from './interfaces/lang';
+
 const isDev = process.env.NODE_ENV === 'development';
 const isDev = process.env.NODE_ENV === 'development';
 
 
 export const i18n = {
 export const i18n = {
-  defaultLocale: 'en_US',
-  locales: ['en_US', 'ja_JP', 'zh_CN'],
+  defaultLocale: Lang.en_US,
+  locales: AllLang,
 };
 };
 export const defaultNS = 'translation';
 export const defaultNS = 'translation';
 export const localePath = path.resolve('./public/static/locales');
 export const localePath = path.resolve('./public/static/locales');

+ 18 - 3
packages/app/src/pages/[[...path]].page.tsx

@@ -5,6 +5,7 @@ import ExtensibleCustomError from 'extensible-custom-error';
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -12,7 +13,6 @@ import { useRouter } from 'next/router';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { PageComments } from '~/components/PageComment/PageComments';
 // import { PageComments } from '~/components/PageComment/PageComments';
 // import { useTranslation } from '~/i18n';
 // import { useTranslation } from '~/i18n';
-import { isPopulated } from '~/interfaces/common';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
 // import { useIndentSize } from '~/stores/editor';
 // import { useIndentSize } from '~/stores/editor';
@@ -21,7 +21,6 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageWithMeta } from '~/interfaces/page';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { PageModel, PageDocument } from '~/server/models/page';
 import { PageModel, PageDocument } from '~/server/models/page';
-import { serializeUserSecurely } from '~/server/models/serializers/user-serializer';
 import UserUISettings, { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import UserUISettings, { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
 import Xss from '~/services/xss';
 import { useSWRxCurrentPage, useSWRxPageInfo, useSWRxPage } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxPageInfo, useSWRxPage } from '~/stores/page';
@@ -30,6 +29,7 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 // import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
 // import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
 
 
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
@@ -52,7 +52,9 @@ import {
 } from '../stores/context';
 } from '../stores/context';
 import { useXss } from '../stores/xss';
 import { useXss } from '../stores/xss';
 
 
-import { CommonProps, getServerSideCommonProps, useCustomTitle } from './commons';
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+} from './commons';
 // import { useCurrentPageSWR } from '../stores/page';
 // import { useCurrentPageSWR } from '../stores/page';
 
 
 
 
@@ -415,6 +417,17 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   };
   };
 }
 }
 
 
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi, user } = req;
   const { crowi, user } = req;
@@ -422,6 +435,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
 
   const result = await getServerSideCommonProps(context);
   const result = await getServerSideCommonProps(context);
 
 
+
   // check for presence
   // check for presence
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   if (!('props' in result)) {
   if (!('props' in result)) {
@@ -445,6 +459,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
 
   injectRoutingInformation(context, props, pageWithMeta);
   injectRoutingInformation(context, props, pageWithMeta);
   injectServerConfigurations(context, props);
   injectServerConfigurations(context, props);
+  injectNextI18NextConfigurations(context, props, ['translation']);
 
 
   if (user != null) {
   if (user != null) {
     props.currentUser = JSON.stringify(user);
     props.currentUser = JSON.stringify(user);

+ 5 - 1
packages/app/src/pages/_app.page.tsx

@@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
 
 
 import { appWithTranslation } from 'next-i18next';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
 import { AppProps } from 'next/app';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
 
 
 import '~/styles/style-next.scss';
 import '~/styles/style-next.scss';
 import '~/styles/theme/default.scss';
 import '~/styles/theme/default.scss';
@@ -32,7 +34,9 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useGrowiVersion(commonPageProps.growiVersion);
   useGrowiVersion(commonPageProps.growiVersion);
 
 
   return (
   return (
-    <Component {...pageProps} />
+    <DndProvider backend={HTML5Backend}>
+      <Component {...pageProps} />
+    </DndProvider>
   );
   );
 }
 }
 
 

+ 24 - 2
packages/app/src/pages/commons.ts

@@ -1,8 +1,11 @@
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
 import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { GetServerSideProps, GetServerSidePropsContext } from 'next';
-
+import { SSRConfig, UserConfig } from 'next-i18next';
 
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
+import { Lang } from '~/interfaces/lang';
+
+import * as nextI18NextConfig from '../next-i18next.config';
 
 
 export type CommonProps = {
 export type CommonProps = {
   namespacesRequired: string[], // i18next
   namespacesRequired: string[], // i18next
@@ -13,7 +16,7 @@ export type CommonProps = {
   customTitleTemplate: string,
   customTitleTemplate: string,
   csrfToken: string,
   csrfToken: string,
   growiVersion: string,
   growiVersion: string,
-}
+} & Partial<SSRConfig>;
 
 
 // eslint-disable-next-line max-len
 // eslint-disable-next-line max-len
 export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
 export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
@@ -41,6 +44,25 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   return { props };
   return { props };
 };
 };
 
 
+export const getNextI18NextConfig = async(
+    // 'serverSideTranslations' method should be given from Next.js Page
+    //  because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
+    serverSideTranslations: (initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null) => Promise<SSRConfig>,
+    context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined,
+): Promise<SSRConfig> => {
+
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi, user } = req;
+  const { configManager } = crowi;
+
+  // determine language
+  const locale = user?.lang
+    ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
+    ?? Lang.en_US;
+
+  return serverSideTranslations(locale, namespacesRequired ?? ['translation'], nextI18NextConfig);
+};
+
 /**
 /**
  * Generate whole title string for the specified title
  * Generate whole title string for the specified title
  * @param props
  * @param props