このコミットが含まれているのは:
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 { 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 <GekanatorUnavailablePage/>
@@ -95,7 +98,7 @@ const GekanatorRoute = ({ user }: { user: User | null }) => {
export const pageRoutes = (
user: User | null,
setUser: Dispatch<SetStateAction<User | null>>,
): RouteObject[] => [
): PageRoute[] => [
{ path: '', element: <LocaleRedirect to="/posts"/> },
{ path: 'posts', element: <PostListPage/> },
{ path: 'posts/new', element: <PostNewPage user={user}/> },
@@ -124,13 +127,24 @@ export const pageRoutes = (
{ path: 'help', element: <HelpListPage/> },
{ path: 'help/changes', element: <HelpChangesPage/> },
{ 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: 'settings', element: <LocaleRedirect to="/users/settings"/> },
{ 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: '*', 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 }: {
+5 -2
ファイルの表示
@@ -74,12 +74,15 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
const dialogue = useDialogue ()
const update = async (...args: Parameters<typeof updatePost>) => {
const update = async (
postData: Parameters<typeof updatePost>[0],
options: Parameters<typeof updatePost>[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,
+1 -1
ファイルの表示
@@ -11,7 +11,7 @@ import type {
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
type ToasterToast = Omit<ToastProps, 'title'> & {
id: string
title?: 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 { 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))
+10 -22
ファイルの表示
@@ -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<number> => {
const matched = matchHelpDetail (url.pathname)
if (matched == null || !(isLocaleCode (matched[1])))
export const prepareHelpPage = async (
{ client, params, locale, searchParams }: PagePreparation,
): Promise<number> => {
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 ({
+2 -2
ファイルの表示
@@ -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)),
+14 -8
ファイルの表示
@@ -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<Params>
locale: LocaleCode
searchParams: URLSearchParams }
export type PageRouteHandle = {
ssr?: (request: PagePreparation) => Promise<number>
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<IndexRouteObject, 'handle'>
| (Omit<NonIndexRouteObject, 'handle' | 'children'> & { children?: PageRoute[] }))
+1 -1
ファイルの表示
@@ -41,7 +41,7 @@
"certified": "公証済み"
},
"languageNames": {
"ja": "日本語",
"ja": "シサㇺ イタㇰ",
"ain": "ヤウンモシルン アイヌ イタㇰ"
},
"revert": "アホシピレ",
+1 -1
ファイルの表示
@@ -31,7 +31,7 @@
},
"languageNames": {
"ja": "日本語",
"ain": "ヤウンモシルン アイヌ イタㇰ"
"ain": "北海道アイヌ語"
},
"revert": "差戻",
"restored": "差戻しました.",
+8 -3
ファイルの表示
@@ -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)
+4 -5
ファイルの表示
@@ -72,11 +72,10 @@ const renderPrimaryName = (
language: string,
label: (key: string) => ReactNode,
) => (
<>
<span lang={language} className="inline-block whitespace-nowrap">{snapshot.name}</span>
{snapshot.name != null && snapshot.autoGenerated === true
&& label ('nameChanges.uncertified')}
</>)
<span lang={language} className="whitespace-nowrap">
{snapshot.name}
{snapshot.name != null && snapshot.autoGenerated && label ('nameChanges.uncertified')}
</span>)
const renderAliasDiff = (