このコミットが含まれているのは:
2026-10-18 23:26:55 +09:00
コミット 8bcc61c0f8
11個のファイルの変更、75行の追加、63行の削除
+22 -8
ファイルの表示
@@ -3,6 +3,7 @@ import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motio
import { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react' import { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { createBrowserRouter, import { createBrowserRouter,
matchRoutes,
Navigate, Navigate,
RouterProvider, RouterProvider,
useLocation, useLocation,
@@ -16,9 +17,10 @@ import DialogueProvider from '@/components/dialogues/DialogueProvider'
import PageHead from '@/components/common/PageHead' import PageHead from '@/components/common/PageHead'
import { Toaster } from '@/components/ui/toaster' import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api' 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 { localesKeys } from '@/lib/queryKeys'
import { requiredPageLocale } from '@/lib/routeLocales'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode, import { applyClientAnimationMode,
applyClientAppearance, applyClientAppearance,
@@ -37,10 +39,11 @@ import HelpDetailPage from '@/pages/help/HelpDetailPage'
import ServiceUnavailable from '@/pages/ServiceUnavailable' import ServiceUnavailable from '@/pages/ServiceUnavailable'
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react' 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 { ClientAnimationMode } from '@/lib/settings'
import type { AvailableLocale } from '@/lib/locales' import type { AvailableLocale } from '@/lib/locales'
import type { PageRoute } from '@/lib/routeLocales'
import type { User } from '@/types' import type { User } from '@/types'
const GekanatorPage = lazy (() => import ('@/pages/GekanatorPage')) const GekanatorPage = lazy (() => import ('@/pages/GekanatorPage'))
@@ -80,7 +83,7 @@ const WikiSearchPage = lazy (() => import ('@/pages/wiki/WikiSearchPage'))
const GekanatorRoute = ({ user }: { user: User | null }) => { const GekanatorRoute = ({ user }: { user: User | null }) => {
const locale = useLocale () const locale = useLocale ()
const location = useLocation () const location = useLocation ()
const pageLocale = requiredPageLocale (location.pathname) const pageLocale = resolvePageRoute (location.pathname)?.route.handle?.locale
if (pageLocale != null && pageLocale !== locale) if (pageLocale != null && pageLocale !== locale)
return <GekanatorUnavailablePage/> return <GekanatorUnavailablePage/>
@@ -95,7 +98,7 @@ const GekanatorRoute = ({ user }: { user: User | null }) => {
export const pageRoutes = ( export const pageRoutes = (
user: User | null, user: User | null,
setUser: Dispatch<SetStateAction<User | null>>, setUser: Dispatch<SetStateAction<User | null>>,
): RouteObject[] => [ ): PageRoute[] => [
{ path: '', element: <LocaleRedirect to="/posts"/> }, { path: '', element: <LocaleRedirect to="/posts"/> },
{ path: 'posts', element: <PostListPage/> }, { path: 'posts', element: <PostListPage/> },
{ path: 'posts/new', element: <PostNewPage user={user}/> }, { path: 'posts/new', element: <PostNewPage user={user}/> },
@@ -124,13 +127,24 @@ export const pageRoutes = (
{ path: 'help', element: <HelpListPage/> }, { path: 'help', element: <HelpListPage/> },
{ path: 'help/changes', element: <HelpChangesPage/> }, { path: 'help/changes', element: <HelpChangesPage/> },
{ path: 'help/:identifier/diff', element: <HelpDiffPage/> }, { path: 'help/:identifier/diff', element: <HelpDiffPage/> },
{ path: 'help/:identifier', element: <HelpDetailPage/> }, { id: 'help.detail', path: 'help/:identifier', element: <HelpDetailPage/>,
handle: { ssr: prepareHelpPage } },
{ path: 'users/settings', element: <SettingPage user={user} setUser={setUser}/> }, { path: 'users/settings', element: <SettingPage user={user} setUser={setUser}/> },
{ path: 'settings', element: <LocaleRedirect to="/users/settings"/> }, { path: 'settings', element: <LocaleRedirect to="/users/settings"/> },
{ path: 'tos', element: <TOSPage/> }, { path: 'tos', element: <TOSPage/> },
{ path: 'gekanator', element: <GekanatorRoute user={user}/> }, { id: 'gekanator', path: 'gekanator', element: <GekanatorRoute user={user}/>,
handle: { locale: 'ja' } },
{ path: 'more', element: <MorePage/> }, { path: 'more', element: <MorePage/> },
{ path: '*', element: <NotFound/> }] { path: '*', element: <NotFound/>, handle: { notFound: true } }]
export const resolvePageRoute = (
pathname: string,
): RouteMatch<string, PageRoute> | null => {
const match = matchRoutes<PageRoute> (
pageRoutes (null, () => { }), unlocalisedPath (pathname))?.at (-1)
return (match == null || match.route.handle?.notFound) ? null : match
}
const RouteTransitionWrapper = ({ animationMode, user, setUser }: { const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
+5 -2
ファイルの表示
@@ -74,12 +74,15 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
const dialogue = useDialogue () const dialogue = useDialogue ()
const update = async (...args: Parameters<typeof updatePost>) => { const update = async (
postData: Parameters<typeof updatePost>[0],
options: Parameters<typeof updatePost>[1],
) => {
clearValidationErrors () clearValidationErrors ()
try try
{ {
const data = await updatePost (...args, undefined, locale) const data = await updatePost (postData, options, locale)
onSave ({ ...post, onSave ({ ...post,
versionNo: data.versionNo, versionNo: data.versionNo,
title: data.title, title: data.title,
+1 -1
ファイルの表示
@@ -11,7 +11,7 @@ import type {
const TOAST_LIMIT = 1 const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000 const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & { type ToasterToast = Omit<ToastProps, 'title'> & {
id: string id: string
title?: React.ReactNode title?: React.ReactNode
description?: React.ReactNode description?: React.ReactNode
+7 -10
ファイルの表示
@@ -2,15 +2,12 @@ import { QueryClient, QueryClientProvider, dehydrate } from '@tanstack/react-que
import { renderToString } from 'react-dom/server' import { renderToString } from 'react-dom/server'
import { HelmetProvider } from 'react-helmet-async' import { HelmetProvider } from 'react-helmet-async'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { matchRoutes } from 'react-router-dom'
import { createStaticHandler, createStaticRouter, import { createStaticHandler, createStaticRouter,
StaticRouterProvider } from 'react-router-dom/server' StaticRouterProvider } from 'react-router-dom/server'
import { AppFrame, appRoutes, pageRoutes } from '@/App' import { AppFrame, appRoutes, resolvePageRoute } from '@/App'
import { matchHelpDetail, prepareHelpPage } from '@/lib/help'
import { createUII18n } from '@/lib/i18n' import { createUII18n } from '@/lib/i18n'
import { fetchLocales, isLocaleCode, legacyLocalePath, import { fetchLocales, isLocaleCode, legacyLocalePath } from '@/lib/locales'
unlocalisedPath } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys' import { localesKeys } from '@/lib/queryKeys'
import { withRenderContext } from '@/lib/renderContext.server' 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 legacy = legacyLocalePath ({ pathname: url.pathname, search: url.search, hash: '' })
const pathname = legacy == null ? url.pathname : new URL (legacy, origin).pathname const pathname = legacy == null ? url.pathname : new URL (legacy, origin).pathname
const segment = pathname.split ('/')[1] const segment = pathname.split ('/')[1]
const known = matchRoutes (pageRoutes (null, () => { }) const known = resolvePageRoute (pathname)
.filter (route => route.path !== '*'), unlocalisedPath (pathname))
if (!(isLocaleCode (segment)) || known == null) if (!(isLocaleCode (segment)) || known == null)
return { status: 404 } return { status: 404 }
if (legacy != null) if (legacy != null)
return { status: 308, location: legacy } return { status: 308, location: legacy }
const match = matchHelpDetail (url.pathname)
const locale = segment const locale = segment
return withRenderContext ({ pathname: url.pathname, origin, apiBaseURL }, async () => { return withRenderContext ({ pathname: url.pathname, origin, apiBaseURL }, async () => {
const client = new QueryClient ({ defaultOptions: { const client = new QueryClient ({ defaultOptions: {
@@ -53,9 +48,11 @@ export const render = async (
const active = locales.locales.some (entry => entry.code === locale) const active = locales.locales.some (entry => entry.code === locale)
if (!(active)) if (!(active))
return { status: 404 as const } 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 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 instance = createUII18n (locale)
const handler = createStaticHandler (appRoutes) const handler = createStaticHandler (appRoutes)
const context = await handler.query (new Request (url)) const context = await handler.query (new Request (url))
+10 -22
ファイルの表示
@@ -1,9 +1,10 @@
import { isLocaleCode, localeLanguage } from '@/lib/locales' import { localeLanguage } from '@/lib/locales'
import { helpKeys } from '@/lib/queryKeys' import { helpKeys } from '@/lib/queryKeys'
import type { QueryClient } from '@tanstack/react-query'
import type { ComponentType } from 'react' import type { ComponentType } from 'react'
import type { PagePreparation } from '@/lib/routeLocales'
type ArticleLanguage = { currentEdition: string; publishedEditions: string[] } type ArticleLanguage = { currentEdition: string; publishedEditions: string[] }
export type ArticleMeta = { export type ArticleMeta = {
@@ -97,34 +98,21 @@ export const resolveHelpLanguage = (
: null : 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. // MDX functions stay in memory; both entries load them before rendering.
export const prepareHelpPage = async (client: QueryClient, url: URL): Promise<number> => { export const prepareHelpPage = async (
const matched = matchHelpDetail (url.pathname) { client, params, locale, searchParams }: PagePreparation,
if (matched == null || !(isLocaleCode (matched[1]))) ): Promise<number> => {
const identifier = params.identifier
if (identifier == null)
return 404 return 404
let identifier: string
try
{
identifier = decodeURIComponent (matched[2])
}
catch
{
return 404
}
const article = await client.fetchQuery ({ const article = await client.fetchQuery ({
queryKey: helpKeys.article (identifier), queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier), staleTime: Infinity }) queryFn: () => getHelpArticle (identifier), staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (matched[1])) const language = resolveHelpLanguage (article, localeLanguage (locale))
if (language == null) if (language == null)
return 404 return 404
const meta = article!.languages[language] 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) if (!(meta.publishedEditions.includes (edition)) || parseHelpEdition (edition) == null)
return 404 return 404
const content = await client.fetchQuery ({ const content = await client.fetchQuery ({
+2 -2
ファイルの表示
@@ -31,7 +31,7 @@ const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ()) ['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
const prefetchHelpIndex: Prefetcher = async (qc, url, locale) => { const prefetchHelpIndex: Prefetcher = async (qc, _url, locale) => {
await qc.prefetchQuery ({ await qc.prefetchQuery ({
queryKey: helpKeys.index (localeLanguage (locale)), queryKey: helpKeys.index (localeLanguage (locale)),
queryFn: () => getHelpSearchEntries (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 ({ await qc.prefetchQuery ({
queryKey: helpKeys.changes (localeLanguage (locale)), queryKey: helpKeys.changes (localeLanguage (locale)),
queryFn: () => getHelpChanges (localeLanguage (locale)), queryFn: () => getHelpChanges (localeLanguage (locale)),
+14 -8
ファイルの表示
@@ -1,13 +1,19 @@
import { matchPath } from 'react-router-dom' import type { QueryClient } from '@tanstack/react-query'
import type { IndexRouteObject, NonIndexRouteObject, Params } from 'react-router-dom'
import { unlocalisedPath } from '@/lib/locales'
import type { LocaleCode } from '@/lib/locales' import type { LocaleCode } from '@/lib/locales'
const PAGE_LOCALES: { path: string; locale: LocaleCode }[] = [ export type PagePreparation = {
{ path: '/gekanator', locale: 'ja' }] client: QueryClient
params: Readonly<Params>
locale: LocaleCode
searchParams: URLSearchParams }
export type PageRouteHandle = {
ssr?: (request: PagePreparation) => Promise<number>
locale?: LocaleCode
notFound?: boolean }
export const requiredPageLocale = (pathname: string): LocaleCode | null => export type PageRoute = { handle?: PageRouteHandle } & (
PAGE_LOCALES.find (page => Omit<IndexRouteObject, 'handle'>
matchPath ({ path: page.path, end: true }, unlocalisedPath (pathname)))?.locale ?? null | (Omit<NonIndexRouteObject, 'handle' | 'children'> & { children?: PageRoute[] }))
+1 -1
ファイルの表示
@@ -41,7 +41,7 @@
"certified": "公証済み" "certified": "公証済み"
}, },
"languageNames": { "languageNames": {
"ja": "日本語", "ja": "シサㇺ イタㇰ",
"ain": "ヤウンモシルン アイヌ イタㇰ" "ain": "ヤウンモシルン アイヌ イタㇰ"
}, },
"revert": "アホシピレ", "revert": "アホシピレ",
+1 -1
ファイルの表示
@@ -31,7 +31,7 @@
}, },
"languageNames": { "languageNames": {
"ja": "日本語", "ja": "日本語",
"ain": "ヤウンモシルン アイヌ イタㇰ" "ain": "北海道アイヌ語"
}, },
"revert": "差戻", "revert": "差戻",
"restored": "差戻しました.", "restored": "差戻しました.",
+8 -3
ファイルの表示
@@ -4,9 +4,8 @@ import { HelmetProvider } from 'react-helmet-async'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import '@/index.css' import '@/index.css'
import App from '@/App' import App, { resolvePageRoute } from '@/App'
import { createUII18n } from '@/lib/i18n' import { createUII18n } from '@/lib/i18n'
import { prepareHelpPage } from '@/lib/help'
import { currentViewingLocale } from '@/lib/locales' import { currentViewingLocale } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys' import { localesKeys } from '@/lib/queryKeys'
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings' import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
@@ -66,7 +65,13 @@ const start = () => {
hydrate (client, data.state) hydrate (client, data.state)
if (client.getQueryData (localesKeys.index) == null) if (client.getQueryData (localesKeys.index) == null)
throw new Error ('Missing SSR locales') 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) if (status !== data.status)
throw new Error ('Server and browser Help versions differ') throw new Error ('Server and browser Help versions differ')
mount (true) mount (true)
+4 -5
ファイルの表示
@@ -72,11 +72,10 @@ const renderPrimaryName = (
language: string, language: string,
label: (key: string) => ReactNode, label: (key: string) => ReactNode,
) => ( ) => (
<> <span lang={language} className="whitespace-nowrap">
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span> {snapshot.name}
{snapshot.name != null && snapshot.autoGenerated === true {snapshot.name != null && snapshot.autoGenerated && label ('nameChanges.uncertified')}
&& label ('nameChanges.uncertified')} </span>)
</>)
const renderAliasDiff = ( const renderAliasDiff = (