diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3c62001..41a36b3 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,6 +3,7 @@ import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motio
import { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { createBrowserRouter,
+ matchRoutes,
Navigate,
RouterProvider,
useLocation,
@@ -16,9 +17,10 @@ import DialogueProvider from '@/components/dialogues/DialogueProvider'
import PageHead from '@/components/common/PageHead'
import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api'
-import { fetchLocales, isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
+import { prepareHelpPage } from '@/lib/help'
+import { fetchLocales, isLocaleCode, legacyLocalePath,
+ switchLocalePath, unlocalisedPath } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys'
-import { requiredPageLocale } from '@/lib/routeLocales'
import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
@@ -37,10 +39,11 @@ import HelpDetailPage from '@/pages/help/HelpDetailPage'
import ServiceUnavailable from '@/pages/ServiceUnavailable'
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
-import type { RouteObject } from 'react-router-dom'
+import type { RouteMatch } from 'react-router-dom'
import type { ClientAnimationMode } from '@/lib/settings'
import type { AvailableLocale } from '@/lib/locales'
+import type { PageRoute } from '@/lib/routeLocales'
import type { User } from '@/types'
const GekanatorPage = lazy (() => import ('@/pages/GekanatorPage'))
@@ -80,7 +83,7 @@ const WikiSearchPage = lazy (() => import ('@/pages/wiki/WikiSearchPage'))
const GekanatorRoute = ({ user }: { user: User | null }) => {
const locale = useLocale ()
const location = useLocation ()
- const pageLocale = requiredPageLocale (location.pathname)
+ const pageLocale = resolvePageRoute (location.pathname)?.route.handle?.locale
if (pageLocale != null && pageLocale !== locale)
return
@@ -95,7 +98,7 @@ const GekanatorRoute = ({ user }: { user: User | null }) => {
export const pageRoutes = (
user: User | null,
setUser: Dispatch>,
-): RouteObject[] => [
+): PageRoute[] => [
{ path: '', element: },
{ path: 'posts', element: },
{ path: 'posts/new', element: },
@@ -124,13 +127,24 @@ export const pageRoutes = (
{ path: 'help', element: },
{ path: 'help/changes', element: },
{ path: 'help/:identifier/diff', element: },
- { path: 'help/:identifier', element: },
+ { id: 'help.detail', path: 'help/:identifier', element: ,
+ handle: { ssr: prepareHelpPage } },
{ path: 'users/settings', element: },
{ path: 'settings', element: },
{ path: 'tos', element: },
- { path: 'gekanator', element: },
+ { id: 'gekanator', path: 'gekanator', element: ,
+ handle: { locale: 'ja' } },
{ path: 'more', element: },
- { path: '*', element: }]
+ { path: '*', element: , handle: { notFound: true } }]
+
+
+export const resolvePageRoute = (
+ pathname: string,
+): RouteMatch | null => {
+ const match = matchRoutes (
+ pageRoutes (null, () => { }), unlocalisedPath (pathname))?.at (-1)
+ return (match == null || match.route.handle?.notFound) ? null : match
+}
const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx
index 7c84726..57c6593 100644
--- a/frontend/src/components/PostEditForm.tsx
+++ b/frontend/src/components/PostEditForm.tsx
@@ -74,12 +74,15 @@ const PostEditForm: FC = ({ post, onSave }) => {
const dialogue = useDialogue ()
- const update = async (...args: Parameters) => {
+ const update = async (
+ postData: Parameters[0],
+ options: Parameters[1],
+ ) => {
clearValidationErrors ()
try
{
- const data = await updatePost (...args, undefined, locale)
+ const data = await updatePost (postData, options, locale)
onSave ({ ...post,
versionNo: data.versionNo,
title: data.title,
diff --git a/frontend/src/components/ui/use-toast.tsx b/frontend/src/components/ui/use-toast.tsx
index dcfbd7d..0762e80 100644
--- a/frontend/src/components/ui/use-toast.tsx
+++ b/frontend/src/components/ui/use-toast.tsx
@@ -11,7 +11,7 @@ import type {
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
-type ToasterToast = ToastProps & {
+type ToasterToast = Omit & {
id: string
title?: React.ReactNode
description?: React.ReactNode
diff --git a/frontend/src/entry-server.tsx b/frontend/src/entry-server.tsx
index f685398..8c4e28e 100644
--- a/frontend/src/entry-server.tsx
+++ b/frontend/src/entry-server.tsx
@@ -2,15 +2,12 @@ import { QueryClient, QueryClientProvider, dehydrate } from '@tanstack/react-que
import { renderToString } from 'react-dom/server'
import { HelmetProvider } from 'react-helmet-async'
import { I18nextProvider } from 'react-i18next'
-import { matchRoutes } from 'react-router-dom'
import { createStaticHandler, createStaticRouter,
StaticRouterProvider } from 'react-router-dom/server'
-import { AppFrame, appRoutes, pageRoutes } from '@/App'
-import { matchHelpDetail, prepareHelpPage } from '@/lib/help'
+import { AppFrame, appRoutes, resolvePageRoute } from '@/App'
import { createUII18n } from '@/lib/i18n'
-import { fetchLocales, isLocaleCode, legacyLocalePath,
- unlocalisedPath } from '@/lib/locales'
+import { fetchLocales, isLocaleCode, legacyLocalePath } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys'
import { withRenderContext } from '@/lib/renderContext.server'
@@ -35,13 +32,11 @@ export const render = async (
const legacy = legacyLocalePath ({ pathname: url.pathname, search: url.search, hash: '' })
const pathname = legacy == null ? url.pathname : new URL (legacy, origin).pathname
const segment = pathname.split ('/')[1]
- const known = matchRoutes (pageRoutes (null, () => { })
- .filter (route => route.path !== '*'), unlocalisedPath (pathname))
+ const known = resolvePageRoute (pathname)
if (!(isLocaleCode (segment)) || known == null)
return { status: 404 }
if (legacy != null)
return { status: 308, location: legacy }
- const match = matchHelpDetail (url.pathname)
const locale = segment
return withRenderContext ({ pathname: url.pathname, origin, apiBaseURL }, async () => {
const client = new QueryClient ({ defaultOptions: {
@@ -53,9 +48,11 @@ export const render = async (
const active = locales.locales.some (entry => entry.code === locale)
if (!(active))
return { status: 404 as const }
- if (match == null || known.at (-1)?.route.path !== 'help/:identifier')
+ const prepare = known.route.handle?.ssr
+ if (prepare == null)
return null
- const status = await prepareHelpPage (client, url)
+ const status = await prepare ({ client, params: known.params, locale,
+ searchParams: url.searchParams })
const instance = createUII18n (locale)
const handler = createStaticHandler (appRoutes)
const context = await handler.query (new Request (url))
diff --git a/frontend/src/lib/help.ts b/frontend/src/lib/help.ts
index 06128a2..e594085 100644
--- a/frontend/src/lib/help.ts
+++ b/frontend/src/lib/help.ts
@@ -1,9 +1,10 @@
-import { isLocaleCode, localeLanguage } from '@/lib/locales'
+import { localeLanguage } from '@/lib/locales'
import { helpKeys } from '@/lib/queryKeys'
-import type { QueryClient } from '@tanstack/react-query'
import type { ComponentType } from 'react'
+import type { PagePreparation } from '@/lib/routeLocales'
+
type ArticleLanguage = { currentEdition: string; publishedEditions: string[] }
export type ArticleMeta = {
@@ -97,34 +98,21 @@ export const resolveHelpLanguage = (
: null
-export const matchHelpDetail = (pathname: string): RegExpMatchArray | null => {
- const matched = pathname.match (/^\/([^/]+)\/help\/([^/]+)\/?$/)
- return matched?.[2] === 'changes' ? null : matched
-}
-
-
// MDX functions stay in memory; both entries load them before rendering.
-export const prepareHelpPage = async (client: QueryClient, url: URL): Promise => {
- const matched = matchHelpDetail (url.pathname)
- if (matched == null || !(isLocaleCode (matched[1])))
+export const prepareHelpPage = async (
+ { client, params, locale, searchParams }: PagePreparation,
+): Promise => {
+ const identifier = params.identifier
+ if (identifier == null)
return 404
- let identifier: string
- try
- {
- identifier = decodeURIComponent (matched[2])
- }
- catch
- {
- return 404
- }
const article = await client.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier), staleTime: Infinity })
- const language = resolveHelpLanguage (article, localeLanguage (matched[1]))
+ const language = resolveHelpLanguage (article, localeLanguage (locale))
if (language == null)
return 404
const meta = article!.languages[language]
- const edition = url.searchParams.get ('edition') ?? meta.currentEdition
+ const edition = searchParams.get ('edition') ?? meta.currentEdition
if (!(meta.publishedEditions.includes (edition)) || parseHelpEdition (edition) == null)
return 404
const content = await client.fetchQuery ({
diff --git a/frontend/src/lib/prefetchers.ts b/frontend/src/lib/prefetchers.ts
index 4bb563d..e78e3d1 100644
--- a/frontend/src/lib/prefetchers.ts
+++ b/frontend/src/lib/prefetchers.ts
@@ -31,7 +31,7 @@ const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
-const prefetchHelpIndex: Prefetcher = async (qc, url, locale) => {
+const prefetchHelpIndex: Prefetcher = async (qc, _url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.index (localeLanguage (locale)),
queryFn: () => getHelpSearchEntries (localeLanguage (locale)),
@@ -63,7 +63,7 @@ const prefetchHelpShow: Prefetcher = async (qc, url, locale) => {
}
-const prefetchHelpChanges: Prefetcher = async (qc, url, locale) => {
+const prefetchHelpChanges: Prefetcher = async (qc, _url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.changes (localeLanguage (locale)),
queryFn: () => getHelpChanges (localeLanguage (locale)),
diff --git a/frontend/src/lib/routeLocales.ts b/frontend/src/lib/routeLocales.ts
index b4f5337..7775314 100644
--- a/frontend/src/lib/routeLocales.ts
+++ b/frontend/src/lib/routeLocales.ts
@@ -1,13 +1,19 @@
-import { matchPath } from 'react-router-dom'
-
-import { unlocalisedPath } from '@/lib/locales'
+import type { QueryClient } from '@tanstack/react-query'
+import type { IndexRouteObject, NonIndexRouteObject, Params } from 'react-router-dom'
import type { LocaleCode } from '@/lib/locales'
-const PAGE_LOCALES: { path: string; locale: LocaleCode }[] = [
- { path: '/gekanator', locale: 'ja' }]
+export type PagePreparation = {
+ client: QueryClient
+ params: Readonly
+ locale: LocaleCode
+ searchParams: URLSearchParams }
+export type PageRouteHandle = {
+ ssr?: (request: PagePreparation) => Promise
+ locale?: LocaleCode
+ notFound?: boolean }
-export const requiredPageLocale = (pathname: string): LocaleCode | null =>
- PAGE_LOCALES.find (page =>
- matchPath ({ path: page.path, end: true }, unlocalisedPath (pathname)))?.locale ?? null
+export type PageRoute = { handle?: PageRouteHandle } & (
+ Omit
+ | (Omit & { children?: PageRoute[] }))
diff --git a/frontend/src/locales/ain-Kana/history.json b/frontend/src/locales/ain-Kana/history.json
index 4ba959c..7170554 100644
--- a/frontend/src/locales/ain-Kana/history.json
+++ b/frontend/src/locales/ain-Kana/history.json
@@ -41,7 +41,7 @@
"certified": "公証済み"
},
"languageNames": {
- "ja": "日本語",
+ "ja": "シサㇺ イタㇰ",
"ain": "ヤウンモシルン アイヌ イタㇰ"
},
"revert": "アホシピレ",
diff --git a/frontend/src/locales/ja/history.json b/frontend/src/locales/ja/history.json
index 3d79308..b61935b 100644
--- a/frontend/src/locales/ja/history.json
+++ b/frontend/src/locales/ja/history.json
@@ -31,7 +31,7 @@
},
"languageNames": {
"ja": "日本語",
- "ain": "ヤウンモシルン アイヌ イタㇰ"
+ "ain": "北海道アイヌ語"
},
"revert": "差戻",
"restored": "差戻しました.",
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index ef50446..61b2186 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -4,9 +4,8 @@ import { HelmetProvider } from 'react-helmet-async'
import { I18nextProvider } from 'react-i18next'
import '@/index.css'
-import App from '@/App'
+import App, { resolvePageRoute } from '@/App'
import { createUII18n } from '@/lib/i18n'
-import { prepareHelpPage } from '@/lib/help'
import { currentViewingLocale } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys'
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
@@ -66,7 +65,13 @@ const start = () => {
hydrate (client, data.state)
if (client.getQueryData (localesKeys.index) == null)
throw new Error ('Missing SSR locales')
- const status = await prepareHelpPage (client, new URL (window.location.href))
+ const url = new URL (window.location.href)
+ const match = resolvePageRoute (url.pathname)
+ const prepare = match?.route.handle?.ssr
+ if (match == null || prepare == null)
+ throw new Error ('Missing SSR route preparation')
+ const status = await prepare ({ client, params: match.params,
+ locale: currentViewingLocale (), searchParams: url.searchParams })
if (status !== data.status)
throw new Error ('Server and browser Help versions differ')
mount (true)
diff --git a/frontend/src/pages/tags/TagHistoryPage.tsx b/frontend/src/pages/tags/TagHistoryPage.tsx
index 5370cae..69b07d0 100644
--- a/frontend/src/pages/tags/TagHistoryPage.tsx
+++ b/frontend/src/pages/tags/TagHistoryPage.tsx
@@ -72,11 +72,10 @@ const renderPrimaryName = (
language: string,
label: (key: string) => ReactNode,
) => (
- <>
- {snapshot.name}
- {snapshot.name != null && snapshot.autoGenerated === true
- && label ('nameChanges.uncertified')}
- >)
+
+ {snapshot.name}
+ {snapshot.name != null && snapshot.autoGenerated && label ('nameChanges.uncertified')}
+ )
const renderAliasDiff = (