このコミットが含まれているのは:
2026-10-17 16:47:08 +09:00
コミット b945ceef60
91個のファイルの変更、4652行の追加、822行の削除
+54 -33
ファイルの表示
@@ -5,7 +5,8 @@ import { createBrowserRouter,
Route,
RouterProvider,
Routes,
useLocation } from 'react-router-dom'
useLocation,
useParams } from 'react-router-dom'
import DevModeWatermark from '@/components/DevModeWatermark'
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
@@ -13,6 +14,8 @@ import TopNav from '@/components/TopNav'
import DialogueProvider from '@/components/dialogues/DialogueProvider'
import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api'
import { isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
fetchUserThemeSlots,
@@ -73,41 +76,41 @@ const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
const routes = (
<Routes location={location}>
<Route path="/" element={<Navigate to="/posts" replace/>}/>
<Route path="/posts" element={<PostListPage/>}/>
<Route path="/posts/new" element={<PostNewPage user={user}/>}/>
<Route path="/posts/search" element={<PostSearchPage/>}/>
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags" element={<TagListPage/>}/>
<Route path="/tags/:id" element={<TagDetailPage/>}/>
<Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/nico/tags" element={<NicoTagListPage user={user}/>}/>
<Route path="/tags/changes" element={<TagHistoryPage/>}/>
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
<Route path="/materials" element={<MaterialBasePage/>}>
<Route path="" element={<LocaleRedirect to="/posts"/>}/>
<Route path="posts" element={<PostListPage/>}/>
<Route path="posts/new" element={<PostNewPage user={user}/>}/>
<Route path="posts/search" element={<PostSearchPage/>}/>
<Route path="posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="posts/changes" element={<PostHistoryPage/>}/>
<Route path="tags" element={<TagListPage/>}/>
<Route path="tags/:id" element={<TagDetailPage/>}/>
<Route path="tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
<Route path="tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="nico/tags" element={<NicoTagListPage user={user}/>}/>
<Route path="tags/changes" element={<TagHistoryPage/>}/>
<Route path="theatres/:id" element={<TheatreDetailPage user={user}/>}/>
<Route path="materials" element={<MaterialBasePage/>}>
<Route index element={<MaterialListPage/>}/>
<Route path="changes" element={<MaterialHistoryPage/>}/>
<Route path="new" element={<MaterialNewPage/>}/>
<Route path="suppressions" element={<MaterialSyncSuppressionsPage/>}/>
<Route path=":id" element ={<MaterialDetailPage/>}/>
</Route>
<Route path="/wiki" element={<WikiSearchPage/>}/>
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
<Route path="/wiki/new" element={<WikiNewPage user={user}/>}/>
<Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
<Route path="/wiki/changes" element={<WikiHistoryPage/>}/>
<Route path="/help" element={<HelpListPage/>}/>
<Route path="/help/changes" element={<HelpChangesPage/>}/>
<Route path="/help/:identifier/diff" element={<HelpDiffPage/>}/>
<Route path="/help/:identifier" element={<HelpDetailPage/>}/>
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
<Route path="/settings" element={<Navigate to="/users/settings" replace/>}/>
<Route path="/tos" element={<TOSPage/>}/>
<Route path="/gekanator" element={<GekanatorPage user={user}/>}/>
<Route path="/more" element={<MorePage/>}/>
<Route path="wiki" element={<WikiSearchPage/>}/>
<Route path="wiki/:title" element={<WikiDetailPage/>}/>
<Route path="wiki/new" element={<WikiNewPage user={user}/>}/>
<Route path="wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
<Route path="wiki/:id/diff" element={<WikiDiffPage/>}/>
<Route path="wiki/changes" element={<WikiHistoryPage/>}/>
<Route path="help" element={<HelpListPage/>}/>
<Route path="help/changes" element={<HelpChangesPage/>}/>
<Route path="help/:identifier/diff" element={<HelpDiffPage/>}/>
<Route path="help/:identifier" element={<HelpDetailPage/>}/>
<Route path="users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
<Route path="settings" element={<LocaleRedirect to="/users/settings"/>}/>
<Route path="tos" element={<TOSPage/>}/>
<Route path="gekanator" element={<GekanatorPage user={user}/>}/>
<Route path="more" element={<MorePage/>}/>
<Route path="*" element={<NotFound/>}/>
</Routes>)
@@ -259,9 +262,27 @@ const RoutedApp: FC = () => {
}
const router = createBrowserRouter ([{
path: '*',
element: <RoutedApp/> }])
const LocaleRoute: FC = () => {
const { locale } = useParams ()
const location = useLocation ()
if (isLocaleCode (locale))
return <RoutedApp/>
const redirect = legacyLocalePath (location)
return redirect == null ? <NotFound/> : <Navigate to={redirect} replace/>
}
const LocaleRedirect = ({ to }: { to: string }) => {
const locale = useLocale ()
const location = useLocation ()
return <Navigate to={switchLocalePath ({ ...location, pathname: to }, locale)} replace/>
}
const router = createBrowserRouter ([
{ path: '/:locale/*', element: <LocaleRoute/> },
{ path: '*', element: <LocaleRoute/> }])
const App: FC = () => (
+13 -9
ファイルの表示
@@ -1,7 +1,8 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import SidebarComponent from '@/components/layout/SidebarComponent'
import TagLink from '@/components/TagLink'
import { fetchMaterialTagTree, parseMaterialFilter } from '@/lib/materials'
@@ -83,7 +84,7 @@ const tagSelectionShellClass = (selected: boolean): string =>
const updateMaterialFilterQuery = (
pathname: string,
locationSearch: string,
navigate: ReturnType<typeof useNavigate>,
navigate: ReturnType<typeof useLocaleNavigate>,
materialFilter: MaterialFilter,
) => {
const qs = new URLSearchParams (locationSearch)
@@ -125,10 +126,11 @@ const MaterialTreeNode: FC<{
tag: MaterialSidebarTag
}> = ({ materialFilter, nestLevel = 0, onChildren, openTags, selectedTagId,
setOpenTags, tag }) => {
const locale = useLocale ()
const open = Boolean (openTags[tag.id])
const { data } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }, locale),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }, locale),
enabled: open && tag.hasChildren && tag.children.length === 0})
useEffect (() => {
@@ -200,6 +202,7 @@ const MobileMaterialTreeNode: FC<{ depth?: number
tag,
},
) => {
const locale = useLocale ()
const open = Boolean (openTags[tag.id])
const tagColumnRef = useRef<HTMLDivElement | null> (null)
const chipRef = useRef<HTMLDivElement | null> (null)
@@ -210,8 +213,8 @@ const MobileMaterialTreeNode: FC<{ depth?: number
const [tagLinkInlineSizePx, setTagLinkInlineSizePx] = useState<number | null> (null)
const [childAvailableInlineSizePx, setChildAvailableInlineSizePx] = useState<number | null> (null)
const { data } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }, locale),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }, locale),
enabled: open && tag.hasChildren && tag.children.length === 0})
useEffect (() => {
@@ -382,8 +385,9 @@ const MobileMaterialTreeNode: FC<{ depth?: number
const MaterialSidebar: FC = () => {
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const qs = new URLSearchParams (location.search)
const materialFilter = parseMaterialFilter (qs.get ('material_filter'), 'any')
const selectedTagId = Number (qs.get ('tag_id') ?? 0) || null
@@ -395,8 +399,8 @@ const MaterialSidebar: FC = () => {
useState<number | null> (null)
const { data: rootTags = [], isLoading, isError } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: null, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter })})
queryKey: materialsKeys.tree ({ parentId: null, materialFilter }, locale),
queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter }, locale)})
useEffect (() => {
setDesktopTags (rootTags)
+3 -1
ファイルの表示
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
import FieldError from '@/components/common/FieldError'
import PostDurationField from '@/components/posts/PostDurationField'
@@ -51,6 +52,7 @@ type Props = { post: Post
const PostEditForm: FC<Props> = ({ post, onSave }) => {
const locale = useLocale ()
const [disabled, setDisabled] = useState (false)
const [duration, setDuration] = useState<string> (videoMsToDurationValue (post.videoMs))
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
@@ -75,7 +77,7 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
try
{
const data = await updatePost (...args)
const data = await updatePost (...args, undefined, locale)
onSave ({ ...post,
versionNo: data.versionNo,
title: data.title,
+3 -1
ファイルの表示
@@ -2,6 +2,7 @@
import { useRef, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import TagSearchBox from '@/components/TagSearchBox'
import FormField from '@/components/common/FormField'
import TextArea from '@/components/common/TextArea'
@@ -38,6 +39,7 @@ type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | '
const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
const locale = useLocale ()
const ref = useRef<HTMLTextAreaElement> (null)
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
@@ -68,7 +70,7 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
setBounds ({ start, end })
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: {
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: {
q: token,
nico: '0' } })
const nextSuggestions = data.filter (t => t.postCount > 0)
+11 -5
ファイルの表示
@@ -1,8 +1,10 @@
import { useQueryClient } from '@tanstack/react-query'
import { forwardRef, useMemo } from 'react'
import { flushSync } from 'react-dom'
import { createPath, useNavigate } from 'react-router-dom'
import { createPath } from 'react-router-dom'
import { localiseTo } from '@/lib/locales'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
@@ -32,13 +34,17 @@ export default forwardRef<HTMLAnchorElement, Props> (({
if ('onClick' in rest)
delete rest['onClick']
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const locale = useLocale ()
const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings ()
const linkPreloadMode = behaviourSettings.linkPreload ?? 'intent'
const path = useMemo (
() => typeof to === 'string' ? to : createPath (to),
[to],
() => {
const localised = localiseTo (to, locale)
return typeof localised === 'string' ? localised : createPath (localised)
},
[to, locale],
)
const url = useMemo (() => {
return (new URL (path, window.location.origin)).toString ()
@@ -98,7 +104,7 @@ export default forwardRef<HTMLAnchorElement, Props> (({
if (!(ok) && cancelOnError)
return
navigate (to, { replace, ...(state && { state }) })
navigate (path, { replace, ...(state && { state }) })
}
catch (ex)
{
+12 -7
ファイルの表示
@@ -12,6 +12,7 @@ import { useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
@@ -27,6 +28,7 @@ import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, originalCreatedAtString } from '@/lib/utils'
import type { LocaleCode } from '@/lib/locales'
import type { CollisionDetection, DragEndEvent } from '@dnd-kit/core'
import type { FC, MutableRefObject, ReactNode } from 'react'
@@ -180,8 +182,10 @@ const buildFlatTagByCategory = (
const changeCategory = async (
tagId: number,
category: Category): Promise<void> => {
await apiPatch (`/tags/${ tagId }`, { category })
category: Category,
locale: LocaleCode = 'ja',
): Promise<void> => {
await apiPatch (`/tags/${ tagId }`, { category }, { locale })
}
@@ -227,6 +231,7 @@ type ActiveTagDrag = {
const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
const locale = useLocale ()
sp = Boolean (sp)
const qc = useQueryClient ()
@@ -271,7 +276,7 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }))
const reloadTags = async (): Promise<void> => {
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`, { locale })))
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
}
@@ -314,9 +319,9 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
return
if (fromParentId != null)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`, { locale })
await apiPost (`/tags/${ parentId }/children/${ childId }`, { })
await apiPost (`/tags/${ parentId }/children/${ childId }`, { }, { locale })
await reloadTags ()
toast ({
@@ -333,10 +338,10 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
return
if (child.category !== cat)
await changeCategory (childId, cat)
await changeCategory (childId, cat, locale)
if (fromParentId != null)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`, { locale })
const fromParent = fromParentId == null ? null : findTag (tags, fromParentId)
+5 -3
ファイルの表示
@@ -1,8 +1,9 @@
// TODO: タグ入力系すべてに同様の処理あるため共通化する.
import { useEffect, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { apiGet } from '@/lib/api'
import { inputClass } from '@/lib/utils'
@@ -14,8 +15,9 @@ import type { Tag } from '@/types'
const TagSearch: FC = () => {
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [activeIndex, setActiveIndex] = useState (-1)
const [search, setSearch] = useState ('')
@@ -32,7 +34,7 @@ const TagSearch: FC = () => {
return
}
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q } })
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: { q } })
setSuggestions (data.filter (t => t.postCount > 0))
if (suggestions.length > 0)
setSuggestionsVsbl (true)
+5 -3
ファイルの表示
@@ -1,7 +1,8 @@
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle'
@@ -22,7 +23,8 @@ type Props = { posts: Post[]
const TagSidebar: FC<Props> = ({ posts, onClick }) => {
const navigate = useNavigate ()
const locale = useLocale ()
const navigate = useLocaleNavigate ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const animationsOff = animationMode === 'off'
@@ -96,7 +98,7 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
try
{
const data = await apiGet<Post> ('/posts/random',
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
{ locale, params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
match: (anyFlg ? 'any' : 'all') } })
navigate (`/posts/${ data.id }`)
}
+82 -52
ファイルの表示
@@ -3,10 +3,18 @@ import { AnimatePresence, motion } from 'framer-motion'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
import TopNavUser from '@/components/TopNavUser'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import { LOCALE_LABELS,
SUPPORTED_LOCALES,
isLocaleCode,
localeFromPath,
switchLocalePath,
unlocalisedPath } from '@/lib/locales'
import { uiMessage, uiMessageLanguage } from '@/lib/messages'
import { materialsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users'
@@ -24,13 +32,16 @@ type NavDensity = 'auto' | 'comfortable' | 'compact'
export const menuOutline = (
{ tag, material, wikiId, user, pathName }: {
{ tag, material, wikiId, user, pathName: fullPathname }: {
tag?: Tag | null
material?: Material | null
wikiId: number | null
user: User | null
pathName: string },
): Menu => {
const pathName = unlocalisedPath (fullPathname)
const locale = localeFromPath (fullPathname) ?? 'ja'
const message = (key: Parameters<typeof uiMessage>[1]) => uiMessage (locale, key)
const postCount = tag?.postCount ?? material?.tag?.postCount ?? 0
const editable = canEditContent (user)
@@ -41,71 +52,73 @@ export const menuOutline = (
const materialFlg = /^\/materials\/\d+/.test (pathName)
const helpVsbl = /\/help\/((?!changes)[^\/?]+)/.test (pathName)
const helpVsbl = /\/help\/((?!changes)[^/?]+)/.test (pathName)
const helpIdentifier = pathName.split ('/')[2] ?? ''
return [
{ name: '広場', to: '/posts', subMenu: [
{ name: '一覧', to: '/posts' },
{ name: '検索', to: '/posts/search' },
{ name: '追加', to: '/posts/new', visible: editable },
{ name: '全体履歴', to: '/posts/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
{ name: 'タグ', to: '/tags', subMenu: [
{ name: 'マスタ', to: '/tags' },
{ name: 'ニコニコ連携', to: '/tags/nico' },
{ name: '全体履歴', to: '/tags/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
{ name: message ('square'), to: '/posts', subMenu: [
{ name: message ('list'), to: '/posts' },
{ name: message ('search'), to: '/posts/search' },
{ name: message ('add'), to: '/posts/new', visible: editable },
{ name: message ('allHistory'), to: '/posts/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:広場' }] },
{ name: message ('tags'), to: '/tags', subMenu: [
{ name: message ('master'), to: '/tags' },
{ name: message ('nicoLinks'), to: '/tags/nico' },
{ name: message ('allHistory'), to: '/tags/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:タグ' },
{ component: <Separator/>, visible: tagFlg },
{ name: `広場 (${ postCount || 0 })`,
to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`,
visible: tagFlg },
{ name: '履歴', to: `/tags/changes?id=${ tag?.id }`,
{ name: message ('history'), to: `/tags/changes?id=${ tag?.id }`,
visible: tagFlg && tag?.category !== 'nico' }] },
{ name: '素材', to: '/materials', visible: true, subMenu: [
{ name: '一覧', to: '/materials' },
{ name: '追加', to: '/materials/new', visible: editable },
{ name: '抑止', to: '/materials/suppressions',
{ name: message ('materials'), to: '/materials', visible: true, subMenu: [
{ name: message ('list'), to: '/materials' },
{ name: message ('add'), to: '/materials/new', visible: editable },
{ name: message ('suppressions'), to: '/materials/suppressions',
visible: user?.role === 'admin' },
{ name: '全体履歴', to: '/materials/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:素材管理' },
{ name: message ('allHistory'), to: '/materials/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:素材管理' },
{ component: <Separator/>, visible: materialFlg },
{ name: `広場 (${ postCount || 0 })`,
to: `/posts?tags=${ encodeURIComponent (material?.tag?.name ?? '') }`,
visible: materialFlg && Boolean (material?.tag) },
{ name: '履歴', to: `/materials/changes?material_id=${ material?.id }`,
{ name: message ('history'), to: `/materials/changes?material_id=${ material?.id }`,
visible: materialFlg }] },
{ name: 'Wiki', to: '/wiki', subMenu: [
{ name: '検索', to: '/wiki' },
{ name: '新規', to: '/wiki/new', visible: editable },
{ name: '全体履歴', to: '/wiki/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' },
{ name: message ('wiki'), to: '/wiki', subMenu: [
{ name: message ('search'), to: '/wiki' },
{ name: message ('create'), to: '/wiki/new', visible: editable },
{ name: message ('allHistory'), to: '/wiki/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:Wiki' },
{ component: <Separator/>, visible: wikiPageFlg },
{ name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
visible: wikiPageFlg },
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`,
{ name: message ('history'), to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ name: message ('edit'), to: `/wiki/${ wikiId || wikiTitle }/edit`,
visible: wikiPageFlg && editable }] },
{ name: 'ヘルプ', to: '/help/home', base: '/help', subMenu: [
{ name: '検索', to: '/help' },
{ name: '全体履歴', to: '/help/changes' },
{ name: message ('help'), to: '/help/home', base: '/help', subMenu: [
{ name: message ('search'), to: '/help' },
{ name: message ('allHistory'), to: '/help/changes' },
{ component: <Separator/>, visible: helpVsbl },
{ name: '履歴',
{ name: message ('history'),
to: `/help/changes?identifier=${ helpIdentifier }`,
visible: helpVsbl }] },
{ name: 'おたのしみ', visible: false, subMenu: [
{ name: '上映会 (β)', to: '/theatres/1' },
{ name: 'グカネータ (β)', to: '/gekanator' }] },
{ name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [
{ name: '一覧', to: '/users', visible: false },
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] },
{ name: '法規', visible: false, subMenu: [
{ name: '利用規約', to: '/tos' }] }]
{ name: message ('entertainment'), visible: false, subMenu: [
{ name: message ('theatre'), to: '/theatres/1' },
{ name: message ('gekanator'), to: '/gekanator' }] },
{ name: message ('users'), to: '/users/settings', visible: false, subMenu: [
{ name: message ('list'), to: '/users', visible: false },
{ name: message ('currentUser'), to: `/users/${ user?.id }`, visible: false },
{ name: message ('settings'), to: '/users/settings', visible: Boolean (user) }] },
{ name: message ('legal'), visible: false, subMenu: [
{ name: message ('terms'), to: '/tos' }] }]
}
const TopNav: FC<Props> = ({ user }) => {
const locale = useLocale ()
const navigate = useLocaleNavigate ()
const location = useLocation ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
@@ -151,25 +164,26 @@ const TopNav: FC<Props> = ({ user }) => {
const { data: wikiPage } = useQuery ({
enabled: Boolean (wikiIdStr),
queryKey: wikiKeys.show (wikiIdStr, { }),
queryFn: () => fetchWikiPage (wikiIdStr, { }) })
queryKey: wikiKeys.show (wikiIdStr, { }, locale),
queryFn: () => fetchWikiPage (wikiIdStr, { }, locale) })
const tagFlg = /^\/tags\/\d+/.test (location.pathname)
const materialFlg = /^\/materials\/\d+/.test (location.pathname)
const pathname = unlocalisedPath (location.pathname)
const tagFlg = /^\/tags\/\d+/.test (pathname)
const materialFlg = /^\/materials\/\d+/.test (pathname)
const effectiveTitle = (((tagFlg || materialFlg)
? location.pathname.split ('/')[2]
? pathname.split ('/')[2]
: wikiPage?.title)
?? '')
const { data: tag } = useQuery ({
enabled: Boolean (effectiveTitle),
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle) })
queryKey: tagsKeys.show (effectiveTitle, locale),
queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle, locale) })
const { data: material } = useQuery ({
enabled: Boolean (effectiveTitle),
queryKey: materialsKeys.show (effectiveTitle),
queryFn: () => fetchMaterial (effectiveTitle) })
queryKey: materialsKeys.show (effectiveTitle, locale),
queryFn: () => fetchMaterial (effectiveTitle, locale) })
const menu = menuOutline ({ tag, material, wikiId, user, pathName: location.pathname })
const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
@@ -177,7 +191,7 @@ const TopNav: FC<Props> = ({ user }) => {
!(item.visible ?? true)
|| item.subMenu.filter (subItem => subItem.visible ?? true).length > 0)
const activeIdx =
visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to))
visibleMenu.findIndex (item => pathname.startsWith (item.base || item.to))
const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40)
const topNavTransition =
animationsOff
@@ -298,7 +312,7 @@ const TopNav: FC<Props> = ({ user }) => {
return (
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
data-desktop={desktop} data-density={density}>
<nav ref={barRef}
<nav ref={barRef} lang={uiMessageLanguage (locale, 'square')}
className="top-nav-root top-nav-bar flex justify-between items-center w-full">
<div className="top-nav-primary flex shrink-0 items-center h-12">
<PrefetchLink
@@ -373,6 +387,22 @@ const TopNav: FC<Props> = ({ user }) => {
</button>
</nav>
<div className="flex min-w-0 justify-end px-2">
<select
aria-label={LOCALE_LABELS[locale]}
value={locale}
className="max-w-full rounded border border-gray-300 bg-white text-xs text-gray-900
dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
onChange={event => {
const nextLocale = event.target.value
if (isLocaleCode (nextLocale))
navigate (switchLocalePath (location, nextLocale))
}}>
{SUPPORTED_LOCALES.map (code => (
<option key={code} value={code}>{LOCALE_LABELS[code]}</option>))}
</select>
</div>
<AnimatePresence initial={false}>
<motion.div
key="submenu-shell"
+4 -2
ファイルの表示
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGFM from 'remark-gfm'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import { wikiKeys } from '@/lib/queryKeys'
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
@@ -26,10 +27,11 @@ const mdComponents = { a: (({ href, children }) => (
const WikiBody: FC<Props> = ({ title, body }) => {
const locale = useLocale ()
const { data } = useQuery ({
enabled: Boolean (body),
queryKey: wikiKeys.index ({ }),
queryFn: () => fetchWikiPages ({ }) })
queryKey: wikiKeys.index ({ }, locale),
queryFn: () => fetchWikiPages ({ }, locale) })
const pageNames = (data ?? []).map (page => page.title).sort ((a, b) => b.length - a.length)
const remarkPlugins = useMemo (
+3 -1
ファイルの表示
@@ -1,5 +1,6 @@
import { useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import TagSearchBox from '@/components/TagSearchBox'
import { apiGet } from '@/lib/api'
import { inputClass } from '@/lib/utils'
@@ -24,6 +25,7 @@ const TagInput: FC<Props> = ({
value,
setValue,
}) => {
const locale = useLocale ()
const [activeIndex, setActiveIndex] = useState (-1)
const [suggestions, setSuggestions] = useState<Tag[]> ([])
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
@@ -39,7 +41,7 @@ const TagInput: FC<Props> = ({
return
}
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: {
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: {
q,
nico: includeNico ? '1' : '0' } })
const nextSuggestions = data.filter (t => t.postCount > 0)
+3 -2
ファイルの表示
@@ -88,8 +88,9 @@ const DialogueProvider: FC<Props> = ({ children }) => {
})
setPendingIds (current => current.filter (pendingId => pendingId !== id))
setFormActions (current => {
const { [id]: _, ...rest } = current
return rest
const rest = { ...current }
delete rest[id]
return rest
})
delete formControls.current[id]
}, [])
+11 -4
ファイルの表示
@@ -5,7 +5,10 @@ import { API_BASE_URL } from '@/config'
import type { AxiosError, AxiosRequestConfig } from 'axios'
import type { LocaleCode } from '@/lib/locales'
type Opt = {
locale?: LocaleCode
params?: AxiosRequestConfig['params']
headers?: Record<string, string>
signal?: AbortSignal
@@ -14,10 +17,14 @@ type Opt = {
const client = axios.create ({ baseURL: API_BASE_URL })
const withUserCode = (opt?: Opt): Opt => ({
...opt,
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '',
...(opt?.headers ?? { }) } })
const withUserCode = (opt?: Opt): AxiosRequestConfig => {
const { locale, ...config } = opt ?? { }
return {
...config,
...(locale != null && { params: { locale, ...config.params } }),
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '',
...(config.headers ?? { }) } }
}
const apiP = async <T> (
+31 -15
ファイルの表示
@@ -1,5 +1,6 @@
import { apiGet, apiPost } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { Post } from '@/types'
export type GekanatorAnswerValue =
@@ -411,24 +412,30 @@ export const storeGekanatorQuestion = (
exampleAnswers: question.exampleAnswers })
export const fetchGekanatorPosts = async (): Promise<Post[]> => {
const data = await apiGet<{ posts: Post[] }> ('/gekanator/posts')
export const fetchGekanatorPosts = async (
locale: LocaleCode = 'ja',
): Promise<Post[]> => {
const data = await apiGet<{ posts: Post[] }> ('/gekanator/posts', { locale })
return data.posts
}
export const fetchGekanatorQuestions = async (): Promise<StoredGekanatorQuestion[]> => {
const data = await apiGet<{ questions: StoredGekanatorQuestion[] }> ('/gekanator/questions')
export const fetchGekanatorQuestions = async (
locale: LocaleCode = 'ja',
): Promise<StoredGekanatorQuestion[]> => {
const data = await apiGet<{ questions: StoredGekanatorQuestion[] }> ('/gekanator/questions', { locale })
return data.questions
}
export const fetchGekanatorExtraQuestions = async (
gameId: number,
nonce?: string): Promise<GekanatorExtraQuestion[]> => {
nonce?: string,
locale: LocaleCode = 'ja',
): Promise<GekanatorExtraQuestion[]> => {
const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> (
`/gekanator/games/${ gameId }/extra_questions`,
{ params: nonce ? { nonce } : undefined })
{ locale, params: nonce ? { nonce } : undefined })
return data.questions
}
@@ -606,7 +613,8 @@ export const buildGekanatorQuestions = (
}
export const saveGekanatorGame = async ({
export const saveGekanatorGame = async (
{
guessedPostId,
correctPostId,
answers,
@@ -614,7 +622,9 @@ export const saveGekanatorGame = async ({
guessedPostId: number
correctPostId: number
answers: GekanatorAnswerLog[]
}): Promise<{ id: number; learnedExampleCount: number }> =>
},
locale: LocaleCode = 'ja',
): Promise<{ id: number; learnedExampleCount: number }> =>
await apiPost ('/gekanator/games', {
guessed_post_id: guessedPostId,
correct_post_id: correctPostId,
@@ -627,10 +637,11 @@ export const saveGekanatorGame = async ({
effective_question: answer.effectiveQuestion,
learning_question: answer.learningQuestion,
answer: answer.answer,
original_answer: answer.originalAnswer })) })
original_answer: answer.originalAnswer })) }, { locale })
export const saveGekanatorQuestionSuggestion = async ({
export const saveGekanatorQuestionSuggestion = async (
{
gekanatorGameId,
existingQuestionId,
questionText,
@@ -640,22 +651,27 @@ export const saveGekanatorQuestionSuggestion = async ({
existingQuestionId?: number
questionText?: string
answer: GekanatorAnswerValue
}): Promise<{ id: number; count: number }> =>
},
locale: LocaleCode = 'ja',
): Promise<{ id: number; count: number }> =>
await apiPost ('/gekanator/question_suggestions', {
gekanator_game_id: gekanatorGameId,
existing_question_id: existingQuestionId,
question_text: questionText,
answer })
answer }, { locale })
export const saveGekanatorExtraQuestionAnswers = async ({
export const saveGekanatorExtraQuestionAnswers = async (
{
gameId,
answers,
}: {
gameId: number
answers: { questionId: number; answer: GekanatorAnswerValue }[]
}) =>
},
locale: LocaleCode = 'ja',
) =>
await apiPost (`/gekanator/games/${ gameId }/extra_question_answers`, {
answers: answers.map (item => ({
question_id: item.questionId,
answer: item.answer })) })
answer: item.answer })) }, { locale })
+16 -3
ファイルの表示
@@ -13,12 +13,14 @@ export type HelpEditionText = {
export type HelpSearchEntry = HelpEditionText & {
identifier: string
language?: string
edition: string }
export type HelpEditionContent = HelpEditionText & { content: ComponentType }
export type HelpEditionSource = HelpEditionText & { source: string }
export type HelpChange = Pick<HelpSearchEntry, 'identifier' | 'edition' | 'title'> & {
language?: string
pred: string | null
change: 'new' | 'update'
date: string }
@@ -82,6 +84,15 @@ export const getHelpArticle = async (identifier: string): Promise<ArticleMeta |
}
export const resolveHelpLanguage = (
article: ArticleMeta | null | undefined,
requestedLanguage: string,
): string =>
article?.languages[requestedLanguage] != null
? requestedLanguage
: article?.canonicalLanguage ?? 'ja'
export const getHelpEditions = async (
identifier: string,
language: string,
@@ -144,7 +155,8 @@ export const getHelpSearchEntries = async (
language: string,
): Promise<HelpSearchEntry[]> => {
const { default: search } = await import ('virtual:help/search')
return search[language] ?? []
const contentLanguage = search[language] != null ? language : 'ja'
return (search[contentLanguage] ?? []).map (entry => ({ ...entry, language: contentLanguage }))
}
@@ -163,13 +175,14 @@ export const getHelpEditionContent = async (
export const getHelpChanges = async (language: string): Promise<HelpChange[]> => {
const { default: changes } = await import ('virtual:help/changes')
return changes[language] ?? []
const contentLanguage = changes[language] != null ? language : 'ja'
return (changes[contentLanguage] ?? []).map (entry => ({ ...entry, language: contentLanguage }))
}
export const getHelpTitleIndex = async (language: string): Promise<HelpTitleIndex> => {
const { default: titles } = await import ('virtual:help/titles')
return titles[language] ?? { }
return titles[language] ?? titles.ja ?? { }
}
+4 -1
ファイルの表示
@@ -1,3 +1,5 @@
import { unlocalisedPath } from '@/lib/locales'
export type ShortcutScope =
| 'global'
| 'posts'
@@ -308,7 +310,8 @@ export const getConflictingShortcutActionIds = (
)
}
export const resolveShortcutScope = (pathname: string): ShortcutScope => {
export const resolveShortcutScope = (fullPathname: string): ShortcutScope => {
const pathname = unlocalisedPath (fullPathname)
if (pathname.startsWith ('/posts/search'))
return 'postSearch'
if (pathname.startsWith ('/posts'))
+55
ファイルの表示
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { isLocaleCode,
legacyLocalePath,
localeFromPath,
localiseTo,
switchLocalePath,
unlocalisedPath } from '@/lib/locales'
describe ('locale URL generation', () => {
it.each (['ja', 'ain-Kana']) ('recognises the supported locale %s', locale => {
expect (isLocaleCode (locale)).toBe (true)
expect (localeFromPath (`/${ locale }/posts`)).toBe (locale)
})
it.each (['en', 'ain', 'JA', '', undefined]) ('rejects %s', locale => {
expect (isLocaleCode (locale)).toBe (false)
})
it.each ([
'/posts', '/posts/new', '/posts/search', '/posts/42', '/posts/changes',
'/tags', '/tags/42', '/tags/42/deerjikists', '/tags/nico', '/nico/tags', '/tags/changes',
'/materials', '/materials/new', '/materials/42', '/materials/changes',
'/materials/suppressions', '/theatres/42', '/wiki', '/wiki/new', '/wiki/title',
'/wiki/42/edit', '/wiki/42/diff', '/wiki/changes', '/help', '/help/changes',
'/help/article', '/help/article/diff', '/users/settings', '/settings',
'/tos', '/gekanator', '/more']) ('localises and redirects %s', pathname => {
const location = { pathname, search: '?page=2&tag=a%23b', hash: '#section' }
expect (localiseTo (pathname, 'ain-Kana')).toBe (`/ain-Kana${ pathname }`)
expect (legacyLocalePath (location)).toBe (`/ja${ pathname }${ location.search }#section`)
expect (unlocalisedPath (`/ain-Kana${ pathname }`)).toBe (pathname)
})
it ('preserves the resource, query and fragment on locale changes', () => {
expect (switchLocalePath ({ pathname: '/ja/tags/42', search: '?x=1&x=2', hash: '#history' },
'ain-Kana')).toBe ('/ain-Kana/tags/42?x=1&x=2#history')
})
it ('preserves explicit locales and relative, fragment and external targets', () => {
for (const target of ['/ja/posts', '/ain-Kana/posts', '?page=2', '#body',
'../changes', 'https://example.com/posts', '//example.com/posts'])
expect (localiseTo (target, 'ain-Kana')).toBe (target)
})
it ('preserves structured targets', () => {
expect (localiseTo ({ pathname: '/tags/42', search: '?q=one', hash: '#two' }, 'ain-Kana'))
.toEqual ({ pathname: '/ain-Kana/tags/42', search: '?q=one', hash: '#two' })
})
it.each (['/en/posts', '/ain/posts', '/unknown', '/api/tags', '/assets/image.png']) (
'does not redirect invalid or non-page paths: %s', pathname => {
expect (legacyLocalePath ({ pathname, search: '', hash: '' })).toBeNull ()
})
})
+62
ファイルの表示
@@ -0,0 +1,62 @@
import type { To } from 'react-router-dom'
export const SUPPORTED_LOCALES = ['ja', 'ain-Kana'] as const
export type LocaleCode = typeof SUPPORTED_LOCALES[number]
export const LOCALE_LABELS: Record<LocaleCode, string> = {
ja: '日本語', 'ain-Kana': 'ヤウンモシルン アイヌ イタㇰ' }
const LEGACY_ROOTS = [
'posts', 'tags', 'nico', 'theatres', 'materials', 'wiki', 'help',
'users', 'settings', 'tos', 'gekanator', 'more']
export const isLocaleCode = (value: unknown): value is LocaleCode =>
SUPPORTED_LOCALES.some (locale => locale === value)
export const localeLanguage = (locale: LocaleCode): 'ja' | 'ain' =>
locale === 'ja' ? 'ja' : 'ain'
export const localeFromPath = (pathname: string): LocaleCode | null => {
const segment = pathname.split (/[/?#]/)[1]
return isLocaleCode (segment) ? segment : null
}
export const unlocalisedPath = (pathname: string): string => {
const locale = localeFromPath (pathname)
return locale == null ? pathname : pathname.slice (locale.length + 1) || '/'
}
export const localiseTo = (to: To, locale: LocaleCode): To => {
const pathname = typeof to === 'string' ? to : to.pathname
if (pathname == null
|| !(pathname.startsWith ('/'))
|| pathname.startsWith ('//')
|| localeFromPath (pathname) != null)
return to
const localised = `/${ locale }${ pathname }`
return typeof to === 'string' ? localised : { ...to, pathname: localised }
}
export const switchLocalePath = (
location: { pathname: string; search: string; hash: string },
locale: LocaleCode,
): string =>
`/${ locale }${ unlocalisedPath (location.pathname) }${ location.search }${ location.hash }`
export const legacyLocalePath = (
location: { pathname: string; search: string; hash: string },
): string | null => {
const root = location.pathname.split ('/')[1]
return (location.pathname === '/' || LEGACY_ROOTS.includes (root))
? `/ja${ location.pathname }${ location.search }${ location.hash }`
: null
}
+38 -17
ファイルの表示
@@ -1,5 +1,6 @@
import { apiGet, isApiError, apiPost, apiPut } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { Material,
MaterialIndexResponse,
MaterialVersion,
@@ -36,8 +37,10 @@ export const fetchMaterials = async (
updatedFrom, updatedTo, sort, direction, page,
tagId, includeDescendants, groupBy,
limit }: FetchMaterialsParams,
locale: LocaleCode = 'ja',
): Promise<MaterialIndexResponse> =>
await apiGet ('/materials', { params: {
await apiGet ('/materials', { locale, params: {
...(q && { q }),
tag_state: tagState,
media_kind: mediaKind,
@@ -54,10 +57,13 @@ export const fetchMaterials = async (
limit} })
export const fetchMaterial = async (id: string): Promise<Material | null> => {
export const fetchMaterial = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Material | null> => {
try
{
return await apiGet (`/materials/${ id }`)
return await apiGet (`/materials/${ id }`, { locale })
}
catch (error)
{
@@ -75,8 +81,10 @@ export const fetchMaterialChanges = async (
eventType?: string
page: number
limit: number
}): Promise<MaterialChangesResponse> =>
await apiGet ('/materials/versions', { params: {
},
locale: LocaleCode = 'ja',
): Promise<MaterialChangesResponse> =>
await apiGet ('/materials/versions', { locale, params: {
...(materialId && { material_id: materialId }),
...(tag && { tag }),
...(eventType && { event_type: eventType }),
@@ -85,19 +93,23 @@ export const fetchMaterialChanges = async (
export const fetchMaterialTagTree = async (
{ parentId, materialFilter }: FetchMaterialTreeParams): Promise<MaterialSidebarTag[]> =>
await apiGet ('/tags/with-depth', { params: {
{ parentId, materialFilter }: FetchMaterialTreeParams,
locale: LocaleCode = 'ja',
): Promise<MaterialSidebarTag[]> =>
await apiGet ('/tags/with-depth', { locale, params: {
...(parentId != null && { parent: String (parentId) }),
material_filter: materialFilter} })
export const fetchMaterialTagByName = async (
name: string,
materialFilter: MaterialFilter): Promise<MaterialTagTree | null> => {
materialFilter: MaterialFilter,
locale: LocaleCode = 'ja',
): Promise<MaterialTagTree | null> => {
try
{
return await apiGet (`/tags/name/${ encodeURIComponent (name) }/materials`,
{ params: { material_filter: materialFilter } })
{ locale, params: { material_filter: materialFilter } })
}
catch (error)
{
@@ -108,19 +120,26 @@ export const fetchMaterialTagByName = async (
}
export const createMaterial = async (formData: FormData): Promise<Material> =>
await apiPost ('/materials', formData)
export const createMaterial = async (
formData: FormData,
locale: LocaleCode = 'ja',
): Promise<Material> =>
await apiPost ('/materials', formData, { locale })
export const updateMaterial = async (
id: string,
formData: FormData): Promise<Material> =>
await apiPut (`/materials/${ id }`, formData)
formData: FormData,
locale: LocaleCode = 'ja',
): Promise<Material> =>
await apiPut (`/materials/${ id }`, formData, { locale })
export const fetchMaterialSyncSuppressions =
async (): Promise<MaterialSyncSuppressionResponse> =>
await apiGet ('/materials/suppressions')
async (
locale: LocaleCode = 'ja',
): Promise<MaterialSyncSuppressionResponse> =>
await apiGet ('/materials/suppressions', { locale })
export const createMaterialSyncSuppression = async (
@@ -130,10 +149,12 @@ export const createMaterialSyncSuppression = async (
drivePath?: string
driveFileId?: string
reason: string
}): Promise<MaterialSyncSuppression> =>
},
locale: LocaleCode = 'ja',
): Promise<MaterialSyncSuppression> =>
await apiPost ('/materials/suppressions', {
source_kind: payload.sourceKind,
source_uri: payload.sourceUri,
drive_path: payload.drivePath,
drive_file_id: payload.driveFileId,
reason: payload.reason})
reason: payload.reason}, { locale })
+38
ファイルの表示
@@ -0,0 +1,38 @@
import type { LocaleCode } from '@/lib/locales'
const japanese = {
square: '広場',
list: '一覧',
search: '検索',
add: '追加',
history: '履歴',
allHistory: '全体履歴',
help: 'ヘルプ',
tags: 'タグ',
master: 'マスタ',
nicoLinks: 'ニコニコ連携',
materials: '素材',
suppressions: '抑止',
wiki: 'Wiki',
create: '新規',
edit: '編輯',
entertainment: 'おたのしみ',
theatre: '上映会 (β)',
gekanator: 'グカネータ (β)',
users: 'ユーザ',
currentUser: 'お前',
settings: '設定',
legal: '法規',
terms: '利用規約' }
export type UIMessageKey = keyof typeof japanese
const ainu: Partial<Record<UIMessageKey, string>> = { }
export const uiMessage = (locale: LocaleCode, key: UIMessageKey): string =>
(locale === 'ain-Kana' ? ainu[key] : null) ?? japanese[key]
export const uiMessageLanguage = (locale: LocaleCode, key: UIMessageKey): 'ja' | 'ain' =>
(locale === 'ain-Kana' && ainu[key] != null) ? 'ain' : 'ja'
+17 -7
ファイルの表示
@@ -1,14 +1,17 @@
import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { FetchPostsParams, Post, PostVersion } from '@/types'
export const fetchPosts = async (
{ url, title, tags, match, createdFrom, createdTo, updatedFrom, updatedTo,
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams): Promise<{
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams,
locale: LocaleCode = 'ja',
): Promise<{
posts: Post[]
count: number }> =>
await apiGet ('/posts', { params: {
await apiGet ('/posts', { locale, params: {
...(url && { url }),
...(title && { title }),
...(tags && { tags }),
@@ -24,7 +27,10 @@ export const fetchPosts = async (
...(order && { order }) } })
export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)
export const fetchPost = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Post> => await apiGet (`/posts/${ id }`, { locale })
export const fetchPostChanges = async (
@@ -33,10 +39,12 @@ export const fetchPostChanges = async (
tag?: string
externalTag?: string
page: number
limit: number }): Promise<{
limit: number },
locale: LocaleCode = 'ja',
): Promise<{
versions: PostVersion[]
count: number }> =>
await apiGet ('/posts/versions', { params: { ...(post && { post }),
await apiGet ('/posts/versions', { locale, params: { ...(post && { post }),
...(tag && { tag }),
...(externalTag && {
external_tag: externalTag }),
@@ -54,7 +62,9 @@ export const updatePost = async (
{ baseVersionNo, force, merge }: {
baseVersionNo?: number
force?: boolean
merge?: boolean }) =>
merge?: boolean },
locale: LocaleCode = 'ja',
) =>
await apiPut<Post> (
`/posts/${ post.id }`,
{ title: post.title,
@@ -63,7 +73,7 @@ export const updatePost = async (
parent_post_ids: post.parentPostIds,
original_created_from: post.originalCreatedFrom,
original_created_before: post.originalCreatedBefore },
{ params: { ...(baseVersionNo && { base_version_no: String (baseVersionNo) }),
{ locale, params: { ...(baseVersionNo && { base_version_no: String (baseVersionNo) }),
force: force ? '1' : '0',
merge: merge ? '1' : '0' } })
+64 -51
ファイルの表示
@@ -5,7 +5,9 @@ import { getHelpArticle,
getHelpChanges,
getHelpDiff,
getHelpEditionContent,
getHelpSearchEntries } from '@/lib/help'
getHelpSearchEntries,
resolveHelpLanguage } from '@/lib/help'
import { localeFromPath, localeLanguage, unlocalisedPath } from '@/lib/locales'
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
@@ -13,9 +15,10 @@ import { fetchWikiPage,
fetchWikiPageByTitle,
fetchWikiPages } from '@/lib/wiki'
import type { LocaleCode } from '@/lib/locales'
import type { Category, FetchPostsOrder, FetchTagsOrder } from '@/types'
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
type Prefetcher = (qc: QueryClient, url: URL, locale: LocaleCode) => Promise<void>
const mPost = match<{ id: string }> ('/posts/:id')
const mWiki = match<{ title: string }> ('/wiki/:title')
@@ -28,25 +31,25 @@ const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
const prefetchHelpIndex: Prefetcher = async qc => {
const prefetchHelpIndex: Prefetcher = async (qc, url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.index ('ja'),
queryFn: () => getHelpSearchEntries ('ja'),
queryKey: helpKeys.index (localeLanguage (locale)),
queryFn: () => getHelpSearchEntries (localeLanguage (locale)),
staleTime: Infinity })
}
const prefetchHelpShow: Prefetcher = async (qc, url) => {
const prefetchHelpShow: Prefetcher = async (qc, url, locale) => {
const matched = mHelp (url.pathname)
if (!(matched))
return
const { identifier } = matched.params
const language = 'ja'
const article = await qc.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
const edition = url.searchParams.get ('edition') ?? article?.languages[language]?.currentEdition
if (edition == null)
return
@@ -58,15 +61,15 @@ const prefetchHelpShow: Prefetcher = async (qc, url) => {
}
const prefetchHelpChanges: Prefetcher = async qc => {
const prefetchHelpChanges: Prefetcher = async (qc, url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.changes ('ja'),
queryFn: () => getHelpChanges ('ja'),
queryKey: helpKeys.changes (localeLanguage (locale)),
queryFn: () => getHelpChanges (localeLanguage (locale)),
staleTime: Infinity })
}
const prefetchHelpDiff: Prefetcher = async (qc, url) => {
const prefetchHelpDiff: Prefetcher = async (qc, url, locale) => {
const matched = mHelpDiff (url.pathname)
const fromValues = url.searchParams.getAll ('from')
const toValues = url.searchParams.getAll ('to')
@@ -76,23 +79,28 @@ const prefetchHelpDiff: Prefetcher = async (qc, url) => {
return
const { identifier } = matched.params
const article = await qc.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
await qc.prefetchQuery ({
queryKey: helpKeys.diff (identifier, 'ja', from, to),
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
queryKey: helpKeys.diff (identifier, language, from, to),
queryFn: () => getHelpDiff (identifier, language, from, to),
staleTime: Infinity })
}
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
const prefetchWikiPagesIndex: Prefetcher = async (qc, url, locale) => {
const title = url.searchParams.get ('title') ?? ''
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ title }),
queryFn: () => fetchWikiPages ({ title }) })
queryKey: wikiKeys.index ({ title }, locale),
queryFn: () => fetchWikiPages ({ title }, locale) })
}
const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
const prefetchWikiPageShow: Prefetcher = async (qc, url, locale) => {
const m = mWiki (url.pathname)
if (!(m))
return
@@ -101,29 +109,29 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
const version = url.searchParams.get ('version') || undefined
const wikiPage = await qc.fetchQuery ({
queryKey: wikiKeys.show (title, { version }),
queryFn: () => fetchWikiPageByTitle (title, { version }) })
queryKey: wikiKeys.show (title, { version }, locale),
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
if (wikiPage)
{
const effectiveId = String (wikiPage.id ?? '')
await qc.prefetchQuery ({
queryKey: wikiKeys.show (effectiveId, { }),
queryFn: () => fetchWikiPage (effectiveId, { } ) })
queryKey: wikiKeys.show (effectiveId, { }, locale),
queryFn: () => fetchWikiPage (effectiveId, { }, locale ) })
if (wikiPage.body)
{
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ }),
queryFn: () => fetchWikiPages ({ }) })
queryKey: wikiKeys.index ({ }, locale),
queryFn: () => fetchWikiPages ({ }, locale) })
}
}
const effectiveTitle = wikiPage?.title ?? title
await qc.prefetchQuery ({
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => fetchTagByName (effectiveTitle) })
queryKey: tagsKeys.show (effectiveTitle, locale),
queryFn: () => fetchTagByName (effectiveTitle, locale) })
if (version)
return
@@ -143,12 +151,12 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
updatedTo: '',
order: 'original_created_at:desc' } as const
await qc.prefetchQuery ({
queryKey: postsKeys.index (p),
queryFn: () => fetchPosts (p) })
queryKey: postsKeys.index (p, locale),
queryFn: () => fetchPosts (p, locale) })
}
const prefetchPostsIndex: Prefetcher = async (qc, url) => {
const prefetchPostsIndex: Prefetcher = async (qc, url, locale) => {
const tags = url.searchParams.get ('tags') ?? ''
const qURL = url.searchParams.get ('url') ?? ''
const title = url.searchParams.get ('title') ?? ''
@@ -169,12 +177,12 @@ const prefetchPostsIndex: Prefetcher = async (qc, url) => {
updatedFrom, updatedTo, order }
await qc.prefetchQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
}
const prefetchPostShow: Prefetcher = async (qc, url) => {
const prefetchPostShow: Prefetcher = async (qc, url, locale) => {
const m = mPost (url.pathname)
if (!(m))
return
@@ -182,12 +190,12 @@ const prefetchPostShow: Prefetcher = async (qc, url) => {
const { id } = m.params
await qc.prefetchQuery ({
queryKey: postsKeys.show (id),
queryFn: () => fetchPost (id) })
queryKey: postsKeys.show (id, locale),
queryFn: () => fetchPost (id, locale) })
}
const prefetchPostChanges: Prefetcher = async (qc, url) => {
const prefetchPostChanges: Prefetcher = async (qc, url, locale) => {
const id = url.searchParams.get ('id')
const tag = url.searchParams.get ('tag')
const externalTag = url.searchParams.get ('external_tag')
@@ -197,23 +205,23 @@ const prefetchPostChanges: Prefetcher = async (qc, url) => {
if (tag)
{
await qc.prefetchQuery ({
queryKey: tagsKeys.show (tag),
queryFn: () => fetchTag (tag) })
queryKey: tagsKeys.show (tag, locale),
queryFn: () => fetchTag (tag, locale) })
}
await qc.prefetchQuery ({
queryKey: postsKeys.changes ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }),
page, limit }, locale),
queryFn: () => fetchPostChanges ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }) })
page, limit }, locale) })
}
const prefetchTagsIndex: Prefetcher = async (qc, url) => {
const prefetchTagsIndex: Prefetcher = async (qc, url, locale) => {
const postRaw = url.searchParams.get ('post')
const post = postRaw ? Number (postRaw) : null
const name = url.searchParams.get ('name') ?? ''
@@ -237,12 +245,12 @@ const prefetchTagsIndex: Prefetcher = async (qc, url) => {
updatedFrom, updatedTo, deprecated, page, limit, order }
await qc.prefetchQuery ({
queryKey: tagsKeys.index (keys),
queryFn: () => fetchTags (keys) })
queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) })
}
const prefetchNicoTagsIndex: Prefetcher = async (qc, url) => {
const prefetchNicoTagsIndex: Prefetcher = async (qc, url, locale) => {
const keys = {
name: url.searchParams.get ('name') ?? '',
linkedTag: url.searchParams.get ('linked_tag') ?? '',
@@ -255,12 +263,12 @@ const prefetchNicoTagsIndex: Prefetcher = async (qc, url) => {
| 'updated_at:asc' | 'updated_at:desc' }
await qc.prefetchQuery ({
queryKey: tagsKeys.nicoIndex (keys),
queryFn: () => fetchNicoTags (keys) })
queryKey: tagsKeys.nicoIndex (keys, locale),
queryFn: () => fetchNicoTags (keys, locale) })
}
const prefetchTagShow: Prefetcher = async (qc, url) => {
const prefetchTagShow: Prefetcher = async (qc, url, locale) => {
const m = mTag (url.pathname)
if (!(m))
return
@@ -268,19 +276,19 @@ const prefetchTagShow: Prefetcher = async (qc, url) => {
const { id } = m.params
await qc.prefetchQuery ({
queryKey: tagsKeys.show (id),
queryFn: () => fetchTag (id) })
queryKey: tagsKeys.show (id, locale),
queryFn: () => fetchTag (id, locale) })
}
const prefetchTagChanges: Prefetcher = async (qc, url) => {
const prefetchTagChanges: Prefetcher = async (qc, url, locale) => {
const id = url.searchParams.get ('id')
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
await qc.prefetchQuery ({
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }) })
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }, locale),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }, locale) })
}
@@ -313,9 +321,14 @@ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[]
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
const u = new URL (urlLike, location.origin)
if (u.origin !== location.origin)
return
const locale = localeFromPath (u.pathname) ?? 'ja'
u.pathname = unlocalisedPath (u.pathname)
const r = routePrefetchers.find (x => x.test (u))
if (!(r))
return
await r.run (qc, u)
await r.run (qc, u, locale)
}
+97 -30
ファイルの表示
@@ -1,3 +1,4 @@
import type { LocaleCode } from '@/lib/locales'
import type {
FetchNicoTagsParams,
FetchMaterialsParams,
@@ -8,59 +9,125 @@ import type {
export const postsKeys = {
root: ['posts'] as const,
index: (p: FetchPostsParams) => ['posts', 'index', p] as const,
show: (id: string) => ['posts', id] as const,
related: (id: string) => ['related', id] as const,
changes: (p: { post?: string
index: (
p: FetchPostsParams,
locale: LocaleCode = 'ja',
) => ['posts', locale, 'index', p] as const,
show: (
id: string,
locale: LocaleCode = 'ja',
) => ['posts', locale, id] as const,
related: (
id: string,
locale: LocaleCode = 'ja',
) => ['related', locale, id] as const,
changes: (
p: { post?: string
tag?: string
externalTag?: string
page: number
limit: number }) =>
['posts', 'changes', p] as const }
limit: number },
locale: LocaleCode = 'ja',
) =>
['posts', locale, 'changes', p] as const }
export const gekanatorKeys = {
root: ['gekanator'] as const,
posts: () => ['gekanator', 'posts'] as const,
questions: () => ['gekanator', 'questions'] as const,
extraQuestions: (gameId: number, nonce: string) =>
['gekanator', 'games', gameId, 'extra-questions', nonce] as const }
posts: (
locale: LocaleCode = 'ja',
) => ['gekanator', locale, 'posts'] as const,
questions: (
locale: LocaleCode = 'ja',
) => ['gekanator', locale, 'questions'] as const,
extraQuestions: (
gameId: number,
nonce: string,
locale: LocaleCode = 'ja',
) =>
['gekanator', locale, 'games', gameId, 'extra-questions', nonce] as const }
export const tagsKeys = {
root: ['tags'] as const,
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
index: (
p: FetchTagsParams,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'index', p] as const,
nicoRoot: ['tags', 'nico'] as const,
nicoIndex: (p: FetchNicoTagsParams) => ['tags', 'nico', 'index', p] as const,
externalShow: (id: string) => ['tags', 'nico', id] as const,
show: (name: string) => ['tags', name] as const,
changes: (p: { id?: string; page: number; limit: number }) =>
['tags', 'changes', p] as const,
deerjikists: (id: string) => ['tags', 'deerjikists', id] as const }
nicoIndex: (
p: FetchNicoTagsParams,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'nico', 'index', p] as const,
externalShow: (
id: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'nico', id] as const,
show: (
name: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, name] as const,
changes: (
p: { id?: string; page: number; limit: number },
locale: LocaleCode = 'ja',
) =>
['tags', locale, 'changes', p] as const,
deerjikists: (
id: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'deerjikists', id] as const }
export const materialsKeys = {
root: ['materials'] as const,
index: (p: FetchMaterialsParams) => ['materials', 'index', p] as const,
changes: (p: {
index: (
p: FetchMaterialsParams,
locale: LocaleCode = 'ja',
) => ['materials', locale, 'index', p] as const,
changes: (
p: {
materialId?: string
tag?: string
eventType?: string
page: number
limit: number
}) => ['materials', 'changes', p] as const,
byTagName: (name: string, materialFilter: MaterialFilter) =>
['materials', 'tag', name, materialFilter] as const,
show: (id: string) => ['materials', id] as const,
suppressions: () => ['materials', 'suppressions'] as const,
tree: (p: {
},
locale: LocaleCode = 'ja',
) => ['materials', locale, 'changes', p] as const,
byTagName: (
name: string,
materialFilter: MaterialFilter,
locale: LocaleCode = 'ja',
) =>
['materials', locale, 'tag', name, materialFilter] as const,
show: (
id: string,
locale: LocaleCode = 'ja',
) => ['materials', locale, id] as const,
suppressions: (
locale: LocaleCode = 'ja',
) => ['materials', locale, 'suppressions'] as const,
tree: (
p: {
parentId?: number | null
materialFilter: MaterialFilter
}) => ['materials', 'tree', p] as const,
unclassified: (p: { page?: number; limit?: number } = { }) =>
['materials', 'unclassified', p] as const}
},
locale: LocaleCode = 'ja',
) => ['materials', locale, 'tree', p] as const,
unclassified: (
p: { page?: number; limit?: number } = { },
locale: LocaleCode = 'ja',
) =>
['materials', locale, 'unclassified', p] as const}
export const wikiKeys = {
root: ['wiki'] as const,
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
index: (
p: { title?: string },
locale: LocaleCode = 'ja',
) => ['wiki', locale, 'index', p] as const,
show: (
title: string,
p: { version?: string },
locale: LocaleCode = 'ja',
) => ['wiki', locale, title, p] as const }
export const helpKeys = {
root: ['help'] as const,
+32 -14
ファイルの表示
@@ -2,6 +2,7 @@ import { isAxiosError } from 'axios'
import { apiGet } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { Deerjikist,
FetchNicoTagsParams,
FetchTagsParams,
@@ -13,9 +14,11 @@ import type { Deerjikist,
export const fetchTags = async (
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
updatedFrom, updatedTo, deprecated,
page, limit, order }: FetchTagsParams): Promise<{ tags: Tag[]
page, limit, order }: FetchTagsParams,
locale: LocaleCode = 'ja',
): Promise<{ tags: Tag[]
count: number }> =>
await apiGet ('/tags', { params: {
await apiGet ('/tags', { locale, params: {
...(post != null && { post }),
...(name && { name }),
...(category && { category }),
@@ -32,9 +35,11 @@ export const fetchTags = async (
export const fetchNicoTags = async (
{ name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams): Promise<{ tags: NicoTag[]
{ name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams,
locale: LocaleCode = 'ja',
): Promise<{ tags: NicoTag[]
count: number }> =>
await apiGet ('/tags/nico', { params: {
await apiGet ('/tags/nico', { locale, params: {
page,
limit,
name,
@@ -43,10 +48,13 @@ export const fetchNicoTags = async (
order } })
export const fetchTag = async (id: string): Promise<Tag | null> => {
export const fetchTag = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/${ id }`)
return await apiGet (`/tags/${ id }`, { locale })
}
catch
{
@@ -55,10 +63,13 @@ export const fetchTag = async (id: string): Promise<Tag | null> => {
}
export const fetchExternalTag = async (id: string): Promise<Tag | null> => {
export const fetchExternalTag = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/nico/${ id }`)
return await apiGet (`/tags/nico/${ id }`, { locale })
}
catch
{
@@ -67,10 +78,13 @@ export const fetchExternalTag = async (id: string): Promise<Tag | null> => {
}
export const fetchTagByName = async (name: string): Promise<Tag | null> => {
export const fetchTagByName = async (
name: string,
locale: LocaleCode = 'ja',
): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/name/${ encodeURIComponent (name) }`)
return await apiGet (`/tags/name/${ encodeURIComponent (name) }`, { locale })
}
catch (error)
{
@@ -84,12 +98,16 @@ export const fetchTagChanges = async (
{ id, page, limit }: {
id?: string
page: number
limit: number }): Promise<{
limit: number },
locale: LocaleCode = 'ja',
): Promise<{
versions: TagVersion[]
count: number }> =>
await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } })
await apiGet ('/tags/versions', { locale, params: { ...(id && { id }), page, limit } })
export const fetchDeerjikistsByTag = async (
id: string): Promise<{ tag: Tag; deerjikists: Deerjikist[] }> =>
await apiGet (`/tags/${ id }/deerjikists`)
id: string,
locale: LocaleCode = 'ja',
): Promise<{ tag: Tag; deerjikists: Deerjikist[] }> =>
await apiGet (`/tags/${ id }/deerjikists`, { locale })
+3 -2
ファイルの表示
@@ -7,8 +7,9 @@ import {
useRef,
useState,
} from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocaleNavigate } from '@/lib/useLocale'
import ShortcutHelpDialog from '@/components/users/ShortcutHelpDialog'
import {
DEFAULT_KEY_BINDINGS,
@@ -95,7 +96,7 @@ const focusSearchTarget = (): void => {
export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [keyboardSettings, setKeyboardSettingsState] =
useState<ClientKeyboardSettings> (() => getClientKeyboardSettings ())
+27
ファイルの表示
@@ -0,0 +1,27 @@
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { isLocaleCode, localeFromPath, localiseTo } from '@/lib/locales'
import type { NavigateFunction, NavigateOptions, To } from 'react-router-dom'
import type { LocaleCode } from '@/lib/locales'
export const useLocale = (): LocaleCode => {
const { locale } = useParams ()
const location = useLocation ()
return isLocaleCode (locale) ? locale : localeFromPath (location.pathname) ?? 'ja'
}
export const useLocaleNavigate = (): NavigateFunction => {
const navigate = useNavigate ()
const locale = useLocale ()
return useCallback ((to: To | number, options?: NavigateOptions) => {
if (typeof to === 'number')
navigate (to)
else
navigate (localiseTo (to, locale), options)
}, [locale, navigate])
}
+13 -6
ファイルの表示
@@ -2,26 +2,33 @@ import { isAxiosError } from 'axios'
import { apiGet } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { WikiPage } from '@/types'
export const fetchWikiPages = async (
{ title }: { title?: string }): Promise<WikiPage[]> =>
await apiGet ('/wiki', { params: { title } })
{ title }: { title?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage[]> =>
await apiGet ('/wiki', { locale, params: { title } })
export const fetchWikiPage = async (
id: string,
{ version }: { version?: string }): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } })
{ version }: { version?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`, { locale, params: version ? { version } : { } })
export const fetchWikiPageByTitle = async (
title: string,
{ version }: { version?: string }): Promise<WikiPage | null> => {
{ version }: { version?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage | null> => {
try
{
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { locale, params: { version } })
}
catch (error)
{
+13 -11
ファイルの表示
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
@@ -3678,6 +3679,7 @@ const expectedAnswerFor = (
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const storedGame = useMemo (loadStoredGame, [])
@@ -3771,8 +3773,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const [history, setHistory] = useState<GameSnapshot[]> ([])
const { data: posts = [], isLoading, error } = useQuery ({
queryKey: gekanatorKeys.posts (),
queryFn: fetchGekanatorPosts,
queryKey: gekanatorKeys.posts (locale),
queryFn: () => fetchGekanatorPosts (locale),
refetchOnWindowFocus: false })
const {
data: acceptedQuestions = [],
@@ -3780,8 +3782,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
isLoading: acceptedQuestionsLoading,
error: acceptedQuestionsError
} = useQuery ({
queryKey: gekanatorKeys.questions (),
queryFn: fetchGekanatorQuestions,
queryKey: gekanatorKeys.questions (locale),
queryFn: () => fetchGekanatorQuestions (locale),
select: questions => questions.map (restoreGekanatorQuestion),
refetchOnWindowFocus: false })
const materialIndex = useMemo (() => buildMaterialIndex (posts), [posts])
@@ -4103,7 +4105,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const mascotAsset = mascotAssetByState[mascot]
const mascotAlt = mascotAltByState[mascot]
const saveMutation = useMutation ({
mutationFn: saveGekanatorGame,
mutationFn: (variables: Parameters<typeof saveGekanatorGame>[0]) => saveGekanatorGame (variables, locale),
onSuccess: (data, variables) => {
setRecentGames (storeRecentGameSummary ({
correctPostId: variables.correctPostId,
@@ -4114,9 +4116,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setLearnedExampleCount (data.learnedExampleCount)
setResultWon (variables.guessedPostId === variables.correctPostId)}})
const questionSuggestionMutation = useMutation ({
mutationFn: saveGekanatorQuestionSuggestion,
mutationFn: (variables: Parameters<typeof saveGekanatorQuestionSuggestion>[0]) => saveGekanatorQuestionSuggestion (variables, locale),
onSuccess: async data => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions (locale) })
setQuestionSuggestionCount (data.count)
setQuestionSuggestionEntryMode ('search')
setQuestionSuggestionSearch ('')
@@ -4124,9 +4126,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setQuestionSuggestion ('')
setQuestionSuggestionAnswer ('yes')}})
const extraQuestionAnswersMutation = useMutation ({
mutationFn: saveGekanatorExtraQuestionAnswers,
mutationFn: (variables: Parameters<typeof saveGekanatorExtraQuestionAnswers>[0]) => saveGekanatorExtraQuestionAnswers (variables, locale),
onSuccess: async () => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions (locale) })
setExtraQuestionState ('saved')
setPhase ('end')}})
@@ -4784,8 +4786,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
try
{
const questions = await queryClient.fetchQuery ({
queryKey: gekanatorKeys.extraQuestions (gameId, nonce),
queryFn: () => fetchGekanatorExtraQuestions (gameId, nonce) })
queryKey: gekanatorKeys.extraQuestions (gameId, nonce, locale),
queryFn: () => fetchGekanatorExtraQuestions (gameId, nonce, locale) })
setExtraQuestions (questions.slice (0, 2))
setExtraQuestionState (questions.length > 0 ? 'ready' : 'empty')
}
+5 -3
ファイルの表示
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
@@ -24,12 +25,13 @@ type DeerjikistFormField =
const DeerjikistDetailPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const tagId = String (id ?? '')
const tagKey = tagsKeys.deerjikists (tagId)
const tagKey = tagsKeys.deerjikists (tagId, locale)
const { data: qData, isLoading: loading } =
useQuery ({ queryKey: tagKey, queryFn: () => fetchDeerjikistsByTag (tagId) })
useQuery ({ queryKey: tagKey, queryFn: () => fetchDeerjikistsByTag (tagId, locale) })
const tag = qData?.tag
const deerjikists = useMemo (() => qData?.deerjikists ?? [], [qData])
@@ -49,7 +51,7 @@ const DeerjikistDetailPage: FC = () => {
{
setDisabled (true)
setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data))
setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data, { locale }))
qc.invalidateQueries ({ queryKey: tagsKeys.root })
toast ({ description: '更新しました.' })
+5 -2
ファイルの表示
@@ -9,6 +9,8 @@ import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { getHelpChanges, helpEditionDate } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { helpKeys } from '@/lib/queryKeys'
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
import { dateString } from '@/lib/utils'
@@ -17,6 +19,7 @@ import type { FC } from 'react'
const HelpChangesPage: FC = () => {
const language = localeLanguage (useLocale ())
const [searchParams, setSearchParams] = useSearchParams ()
const identifier = searchParams.get ('identifier') ?? ''
const pageValue = searchParams.get ('page') ?? '1'
@@ -27,8 +30,8 @@ const HelpChangesPage: FC = () => {
const limit = LIST_LIMIT_OPTIONS.find (
value => String (value) === searchParams.get ('limit')) ?? 20
const { data: changes = [], isLoading, error } = useQuery ({
queryKey: helpKeys.changes ('ja'),
queryFn: () => getHelpChanges ('ja'),
queryKey: helpKeys.changes (language),
queryFn: () => getHelpChanges (language),
staleTime: Infinity })
const results = useMemo (() => identifier
+8 -5
ファイルの表示
@@ -8,7 +8,10 @@ import { SITE_TITLE } from '@/config'
import { getHelpArticle,
getHelpEditionContent,
helpEditionDate,
parseHelpEdition } from '@/lib/help'
parseHelpEdition,
resolveHelpLanguage } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { dateString } from '@/lib/utils'
import { helpKeys } from '@/lib/queryKeys'
import NotFound from '@/pages/NotFound'
@@ -17,17 +20,17 @@ import type { FC } from 'react'
const HelpDetailPage: FC = () => {
const locale = useLocale ()
const { identifier = '' } = useParams ()
const location = useLocation ()
// Locale 対応が入るまでは ja 固定
const language = 'ja'
const { data: article, isLoading: articleLoading } = useQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
const query = new URLSearchParams (location.search)
const requestedEdition = query.get ('edition')
@@ -96,7 +99,7 @@ const HelpDetailPage: FC = () => {
: '(最新)'}
</div>)}
<article className="prose dark:prose-invert mx-auto p-4">
<article lang={language} className="prose dark:prose-invert mx-auto p-4">
{Content ? <Content/> : <div>Loading...</div>}
</article>
</MainArea>)
+13 -5
ファイルの表示
@@ -6,7 +6,9 @@ import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { getHelpDiff } from '@/lib/help'
import { getHelpArticle, getHelpDiff, resolveHelpLanguage } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { helpKeys } from '@/lib/queryKeys'
import { cn } from '@/lib/utils'
import NotFound from '@/pages/NotFound'
@@ -17,17 +19,23 @@ const markers = { added: '+ ', removed: '- ', unchanged: ' ' }
const HelpDiffPage: FC = () => {
const locale = useLocale ()
const { identifier = '' } = useParams ()
const [searchParams] = useSearchParams ()
const from = searchParams.get ('from') ?? ''
const to = searchParams.get ('to') ?? ''
const { data: article } = useQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
const validParams = Boolean (identifier && from && to)
&& searchParams.getAll ('from').length === 1
&& searchParams.getAll ('to').length === 1
const { data: diff, isLoading, isError } = useQuery ({
enabled: validParams,
queryKey: helpKeys.diff (identifier, 'ja', from, to),
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
enabled: validParams && article != null,
queryKey: helpKeys.diff (identifier, language, from, to),
queryFn: () => getHelpDiff (identifier, language, from, to),
staleTime: Infinity })
if (!(validParams) || isError || (!(isLoading) && diff == null))
@@ -55,7 +63,7 @@ const HelpDiffPage: FC = () => {
{diff.to.title} ({to})
</PrefetchLink>
</div>
<pre className="mt-4 overflow-x-auto whitespace-pre">
<pre lang={language} className="mt-4 overflow-x-auto whitespace-pre">
<code>
{diff.lines.map ((line, index) => {
const colour = cn (
+5 -2
ファイルの表示
@@ -15,6 +15,8 @@ import { compareHelpEditions,
getHelpSearchEntries,
helpEditionDate,
normaliseHelpSearchText } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { helpKeys } from '@/lib/queryKeys'
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
import { dateString, inputClass } from '@/lib/utils'
@@ -30,6 +32,7 @@ const defaultDirection: Record<HelpOrderField, 'asc' | 'desc'> = {
const HelpSearchPage: FC = () => {
const language = localeLanguage (useLocale ())
const [searchParams, setSearchParams] = useSearchParams ()
const pageValue = searchParams.get ('page') ?? '1'
const pageNumber = Number (pageValue)
@@ -49,8 +52,8 @@ const HelpSearchPage: FC = () => {
const [title, setTitle] = useState (conditions.title)
const [text, setText] = useState (conditions.text)
const { data: entries = [], isLoading: loading, error } = useQuery ({
queryKey: helpKeys.index ('ja'),
queryFn: () => getHelpSearchEntries ('ja'),
queryKey: helpKeys.index (language),
queryFn: () => getHelpSearchEntries (language),
staleTime: Infinity })
useEffect (() => {
+6 -4
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import WikiBody from '@/components/WikiBody'
import FieldError from '@/components/common/FieldError'
@@ -28,6 +29,7 @@ type MaterialFormField = 'tag' | 'file' | 'url' | 'exportPaths'
const MaterialDetailPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const qc = useQueryClient ()
@@ -40,8 +42,8 @@ const MaterialDetailPage: FC = () => {
useValidationErrors<MaterialFormField> ()
const { data: material, isError, isLoading } = useQuery ({
queryKey: materialsKeys.show (id ?? ''),
queryFn: () => fetchMaterial (id ?? ''),
queryKey: materialsKeys.show (id ?? '', locale),
queryFn: () => fetchMaterial (id ?? '', locale),
enabled: id != null})
const materialTitle = material
? material.tag?.name ?? `素材 #${ material.id }`
@@ -76,10 +78,10 @@ const MaterialDetailPage: FC = () => {
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
return await updateMaterial (id ?? '', formData)
return await updateMaterial (id ?? '', formData, locale)
},
onSuccess: async data => {
qc.setQueryData (materialsKeys.show (id ?? ''), data)
qc.setQueryData (materialsKeys.show (id ?? '', locale), data)
await invalidateMaterialQueries ()
toast ({ title: '更新成功!' })
},
+6 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
@@ -19,10 +20,11 @@ import type { FC, FormEvent } from 'react'
const MaterialHistoryPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = new URLSearchParams (location.search)
const materialId = query.get ('material_id') ?? ''
@@ -47,13 +49,13 @@ const MaterialHistoryPage: FC = () => {
...(tag && { tag }),
...(eventType && { eventType }),
page,
limit }),
limit }, locale),
queryFn: () => fetchMaterialChanges ({
...(materialId && { materialId }),
...(tag && { tag }),
...(eventType && { eventType }),
page,
limit }) })
limit }, locale) })
const versions = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+6 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import nikumaru from '@/assets/fonts/nikumaru.otf'
import PrefetchLink from '@/components/PrefetchLink'
import DateTimeField from '@/components/common/DateTimeField'
@@ -210,8 +211,9 @@ const GroupHeading: FC<{
const MaterialListPage: FC = () => {
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
const materialFilter = parseMaterialFilter (query.get ('material_filter'), 'present')
@@ -277,8 +279,8 @@ const MaterialListPage: FC = () => {
page,
limit}
const { data, isLoading, isError } = useQuery ({
queryKey: materialsKeys.index (keys),
queryFn: () => fetchMaterials (keys)})
queryKey: materialsKeys.index (keys, locale),
queryFn: () => fetchMaterials (keys, locale)})
const tagScope = data?.tagScope ?? null
const materials = data?.materials ?? []
const groups = data?.groups ?? []
+8 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
@@ -13,6 +14,7 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { createMaterial } from '@/lib/materials'
import { unlocalisedPath } from '@/lib/locales'
import { materialsKeys } from '@/lib/queryKeys'
import { inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
@@ -23,14 +25,16 @@ type MaterialFormField = 'tag' | 'file' | 'url' | 'exportPaths'
const MaterialNewPage: FC = () => {
const locale = useLocale ()
const qc = useQueryClient ()
const location = useLocation ()
const query = new URLSearchParams (location.search)
const tagQuery = query.get ('tag') ?? ''
const returnToQuery = query.get ('return_to') ?? ''
const safeReturnTo = returnToQuery.startsWith ('/materials') ? returnToQuery : ''
const safeReturnTo =
unlocalisedPath (returnToQuery).startsWith ('/materials') ? returnToQuery : ''
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [file, setFile] = useState<File | null> (null)
const [filePreview, setFilePreview] = useState ('')
@@ -51,7 +55,7 @@ const MaterialNewPage: FC = () => {
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
return await createMaterial (formData)
return await createMaterial (formData, locale)
},
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
+6 -4
ファイルの表示
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
@@ -52,6 +53,7 @@ const reasonName = (reason: string): string =>
const MaterialSyncSuppressionsPage: FC = () => {
const locale = useLocale ()
const qc = useQueryClient ()
const [sourceKind, setSourceKind] =
useState<MaterialSyncSuppressionSourceKind> ('uri')
@@ -61,8 +63,8 @@ const MaterialSyncSuppressionsPage: FC = () => {
const [reason, setReason] = useState<MaterialSyncSuppressionReason> (REASONS[0])
const { data, isError, isLoading } = useQuery ({
queryKey: materialsKeys.suppressions (),
queryFn: fetchMaterialSyncSuppressions})
queryKey: materialsKeys.suppressions (locale),
queryFn: () => fetchMaterialSyncSuppressions (locale)})
const createMutation = useMutation ({
mutationFn: async () =>
@@ -71,9 +73,9 @@ const MaterialSyncSuppressionsPage: FC = () => {
sourceUri,
drivePath,
driveFileId,
reason}),
reason}, locale),
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions () })
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions (locale) })
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
setSourceUri ('')
setDrivePath ('')
+5 -3
ファイルの表示
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PostEditForm from '@/components/PostEditForm'
import PostEmbed from '@/components/PostEmbed'
import PostList from '@/components/PostList'
@@ -29,6 +30,7 @@ type Props = { user: User | null }
const PostDetailPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const behaviourSettings = useClientBehaviourSettings ()
const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal'
@@ -42,12 +44,12 @@ const PostDetailPage: FC<Props> = ({ user }) => {
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const postId = String (id ?? '')
const postKey = postsKeys.show (postId)
const postKey = postsKeys.show (postId, locale)
const { data: post, isError: errorFlg, error } = useQuery ({
enabled: Boolean (id),
queryKey: postKey,
queryFn: () => fetchPost (postId) })
queryFn: () => fetchPost (postId, locale) })
const qc = useQueryClient ()
@@ -181,7 +183,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
<PostEditForm
post={post}
onSave={newPost => {
qc.setQueryData (postsKeys.show (postId),
qc.setQueryData (postsKeys.show (postId, locale),
(prev: Post | undefined) => newPost ?? prev)
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
+9 -7
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
@@ -39,6 +40,7 @@ const renderDiff = (diff: { current: string | null; prev: string | null }) => (
const PostHistoryPage: FC = () => {
const locale = useLocale ()
const dialogue = useDialogue ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
@@ -61,25 +63,25 @@ const PostHistoryPage: FC = () => {
const tagQueryId = tagId ?? ''
const { data: tag } = useQuery ({
enabled: Boolean (tagId),
queryKey: tagsKeys.show (tagQueryId),
queryFn: () => fetchTag (tagQueryId) })
queryKey: tagsKeys.show (tagQueryId, locale),
queryFn: () => fetchTag (tagQueryId, locale) })
const externalTagQueryId = externalTagId ?? ''
const { data: externalTag } = useQuery ({
enabled: Boolean (externalTagId),
queryKey: tagsKeys.externalShow (externalTagQueryId),
queryFn: () => fetchExternalTag (externalTagQueryId) })
queryKey: tagsKeys.externalShow (externalTagQueryId, locale),
queryFn: () => fetchExternalTag (externalTagQueryId, locale) })
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.changes ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && { externalTag: externalTagId }),
page, limit }),
page, limit }, locale),
queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && {
externalTag: externalTagId }),
page, limit }) })
page, limit }, locale) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
const displayedTag = externalTag ?? tag
@@ -121,7 +123,7 @@ const PostHistoryPage: FC = () => {
const originalCreatedBefore = change.originalCreatedBefore.current
await updatePost ({ id, title, tags, duration, parentPostIds,
originalCreatedFrom, originalCreatedBefore },
{ force: true })
{ force: true }, locale)
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
+10 -8
ファイルの表示
@@ -2,8 +2,9 @@ import { AnimatePresence, motion } from 'framer-motion'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { ChevronRight } from 'lucide-react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PrefetchLink from '@/components/PrefetchLink'
@@ -595,10 +596,11 @@ const editableRowDirty = (row: PostImportRow): boolean =>
const PostImportReviewPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const dialogue = useDialogue ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const existingRowsTransition =
@@ -696,7 +698,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
try
{
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', { locale,
params: { url: currentRow.url },
signal: controller.signal })
if (!(active) || metadataSequenceRef.current !== sequence)
@@ -770,7 +772,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
active = false
controllers.forEach (controller => controller.abort ())
}
}, [commitRows, currentRowBySource, location.search, updateRows])
}, [commitRows, currentRowBySource, location.search, updateRows, locale])
useEffect (() => {
if (editingRow == null || resultRepairMode (rowsRef.current) !== 'failed')
@@ -871,12 +873,12 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
urlChanged
? mergePreviewRow (
nextRow,
await apiGet<PostMetadataResponse> ('/posts/metadata', {
await apiGet<PostMetadataResponse> ('/posts/metadata', { locale,
params: { url: nextRow.url } }))
: nextRow
const dryRun = await apiPost<PostMetadataResponse> (
'/posts?dry=1',
buildDryRunFormData (candidateRow))
buildDryRunFormData (candidateRow), { locale })
const mergedRow = mergeDryRunRow (candidateRow, dryRun)
const mergedRows = updateRows (currentRows =>
replaceImportRow (currentRows, mergedRow))
@@ -974,7 +976,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const result = await apiPost<{ results: BulkApiRow[] }>(
'/posts/bulk',
buildBulkFormData ([target]))
buildBulkFormData ([target]), { locale })
if (result.results.length !== 1)
{
updateRows (currentRows => replaceImportRow (currentRows, originalRow))
@@ -1018,7 +1020,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
{
const result = await apiPost<{ results: BulkApiRow[] }>(
'/posts/bulk',
buildBulkFormData (latestProcessingRows))
buildBulkFormData (latestProcessingRows), { locale })
if (result.results.length !== latestProcessingRows.length)
{
toast ({ title: '登録結果が不完全でした' })
+3 -2
ファイルの表示
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom'
import { useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
@@ -40,7 +41,7 @@ const SOURCE_ISSUES_ID = 'post-import-source-issues'
const PostImportSourcePage: FC<Props> = ({ user }) => {
const editable = canEditContent (user)
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [source, setSource] = useState ('')
const [loading, setLoading] = useState (false)
const [sourceIssues, setSourceIssues] = useState<ReturnType<typeof validateImportSource>> ([])
+8 -6
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PostList from '@/components/PostList'
import PrefetchLink from '@/components/PrefetchLink'
import TagSidebar from '@/components/TagSidebar'
@@ -33,12 +34,13 @@ const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
const PostListPage: FC = () => {
const locale = useLocale ()
const containerRef = useRef<HTMLDivElement | null> (null)
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = new URLSearchParams (location.search)
const tagsQuery = query.get ('tags') ?? ''
const anyFlg = query.get ('match') === 'any'
@@ -59,8 +61,8 @@ const PostListPage: FC = () => {
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
order } as const
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const posts = data?.posts ?? []
const cursor = ''
const totalPages = data ? Math.ceil (data.count / limit) : 0
@@ -111,14 +113,14 @@ const PostListPage: FC = () => {
try
{
const tagName = tags[0]
setWikiPage (await fetchWikiPageByTitle (tagName, { }))
setWikiPage (await fetchWikiPageByTitle (tagName, { }, locale))
}
catch
{
;
}
}) ()
}, [location.search, tags])
}, [location.search, tags, locale])
return (
<div
+6 -4
ファイルの表示
@@ -2,8 +2,9 @@ import { useQuery } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink'
@@ -70,6 +71,7 @@ const parseOrder = (value: string | null): FetchPostsOrder | null => {
const PostSearchPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const thumbnailTransition = clientAnimationTransition (
@@ -79,7 +81,7 @@ const PostSearchPage: FC = () => {
)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search),
[location.search])
@@ -130,8 +132,8 @@ const PostSearchPage: FC = () => {
updatedTo: qUpdatedTo,
order }
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const results = data?.posts ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+7 -5
ファイルの表示
@@ -2,8 +2,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, LoaderCircle, Pencil, X } from 'lucide-react'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import SortHeader from '@/components/SortHeader'
import FieldError from '@/components/common/FieldError'
@@ -40,11 +41,12 @@ const setIf = (qs: URLSearchParams, key: string, value: string) => {
const NicoTagListPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const dialogue = useDialogue ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const queryClient = useQueryClient ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
@@ -66,8 +68,8 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
const keys = {
name: qName, linkedTag: qLinkedTag, linkStatus: qLinkStatus, page, limit, order }
const { data, isError, isLoading: loading } = useQuery ({
queryKey: tagsKeys.nicoIndex (keys),
queryFn: () => fetchNicoTags (keys) })
queryKey: tagsKeys.nicoIndex (keys, locale),
queryFn: () => fetchNicoTags (keys, locale) })
const nicoTags = data?.tags ?? []
const count = data?.count ?? 0
@@ -128,7 +130,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
try
{
await apiPut<Tag[]> (`/tags/nico/${ id }`, formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
setErrorsByTagId (errors => ({ ...errors, [id]: [] }))
setEditingId (null)
await queryClient.invalidateQueries ({ queryKey: tagsKeys.nicoRoot })
+5 -3
ファイルの表示
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
@@ -28,13 +29,14 @@ type TagFormField =
const TagDetailPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const tagId = String (id ?? '')
const tagKey = tagsKeys.show (tagId)
const tagKey = tagsKeys.show (tagId, locale)
const { data: tag, isLoading: loading } = useQuery ({
queryKey: tagKey,
queryFn: () => fetchTag (tagId) })
queryFn: () => fetchTag (tagId, locale) })
const [name, setName] = useState ('')
const [category, setCategory] = useState<Category> ('general')
@@ -60,7 +62,7 @@ const TagDetailPage: FC = () => {
try
{
const data = await apiPut<Tag> (`/tags/${ id }`, formData)
const data = await apiPut<Tag> (`/tags/${ id }`, formData, { locale })
setName (data.name)
setCategory (data.category as Category)
+5 -3
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
@@ -45,6 +46,7 @@ const renderStateDiff = (diff: { current: string | null; prev: string | null })
const TagHistoryPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
@@ -54,8 +56,8 @@ const TagHistoryPage: FC = () => {
const limit = Number (query.get ('limit') ?? 20)
const { data, isLoading: loading } = useQuery ({
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }) })
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }, locale),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }, locale) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
@@ -210,7 +212,7 @@ const TagHistoryPage: FC = () => {
change.parentTags
.filter (t => t.type !== 'removed')
.map (t => t.tag.name)
.join (' ') })
.join (' ') }, { locale })
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
+6 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink'
@@ -73,11 +74,12 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
const TagListPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
@@ -129,8 +131,8 @@ const TagListPage: FC = () => {
updatedTo: qUpdatedTo,
deprecated: qDeprecated }
const { data, isLoading: loading } = useQuery ({
queryKey: tagsKeys.index (keys),
queryFn: () => fetchTags (keys) })
queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) })
const results = data?.tags ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+20 -18
ファイルの表示
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import ErrorScreen from '@/components/ErrorScreen'
import PostEditForm from '@/components/PostEditForm'
import PostEmbed from '@/components/PostEmbed'
@@ -215,6 +216,7 @@ type Props = { user: User | null }
const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
@@ -286,16 +288,16 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return
setProgrammes (await apiGet<TheatreProgramme[]> (
`/theatres/${ id }/programmes`, { params: { limit: '100' } }))
}, [id])
`/theatres/${ id }/programmes`, { locale, params: { limit: '100' } }))
}, [id, locale])
const refreshWeights = useCallback (async () => {
if (!(id))
return
setWeights (await apiGet<TheatrePostSelectionWeights> (
`/theatres/${ id }/post_selection_weights`))
}, [id])
`/theatres/${ id }/post_selection_weights`, { locale }))
}, [id, locale])
const advancePost = useCallback (async () => {
if (!(id))
@@ -304,7 +306,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
setLoading (true)
try
{
await apiPatch<void> (`/theatres/${ id }/next_post`)
await apiPatch<void> (`/theatres/${ id }/next_post`, { }, { locale })
await refreshProgrammes ()
await refreshWeights ()
}
@@ -316,7 +318,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
{
setLoading (false)
}
}, [id, refreshProgrammes, refreshWeights])
}, [id, refreshProgrammes, refreshWeights, locale])
useEffect (() => {
loadingRef.current = loading
@@ -365,7 +367,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
void (async () => {
try
{
const data = await apiGet<Theatre> (`/theatres/${ id }`)
const data = await apiGet<Theatre> (`/theatres/${ id }`, { locale })
if (!(cancelled))
setTheatre (data)
}
@@ -381,7 +383,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return () => {
cancelled = true
}
}, [id, refreshProgrammes, refreshWeights])
}, [id, refreshProgrammes, refreshWeights, locale])
useEffect (() => {
if (!(id))
@@ -400,7 +402,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
{
const newComments = await apiGet<TheatreComment[]> (
`/theatres/${ id }/comments`,
{ params: { no_gt: lastCommentNoRef.current, limit: '20' } })
{ locale, params: { no_gt: lastCommentNoRef.current, limit: '20' } })
if (!(cancelled) && newComments.length > 0)
{
@@ -433,7 +435,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
}
const watchingRequestedAt = performance.now ()
const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`)
const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`, { }, { locale })
if (!(cancelled) && watchingRequestedAt >= theatreInfoReceivedAtRef.current)
applyTheatreInfo (nextInfo)
}
@@ -454,7 +456,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
cancelled = true
clearInterval (interval)
}
}, [applyTheatreInfo, currentPostElapsedMs, id])
}, [applyTheatreInfo, currentPostElapsedMs, id, locale])
useEffect (() => {
if (!(id) || !(theatreInfo.hostFlg) || loadingRef.current || theatreInfo.postId != null)
@@ -487,7 +489,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
void (async () => {
try
{
const nextPost = await fetchPost (String (theatreInfo.postId))
const nextPost = await fetchPost (String (theatreInfo.postId), locale)
if (!(cancelled))
setPost (nextPost)
}
@@ -500,7 +502,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return () => {
cancelled = true
}
}, [theatreInfo.postId])
}, [theatreInfo.postId, locale])
useEffect (() => {
void refreshProgrammes ()
@@ -570,9 +572,9 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const nextInfo =
theatreInfo.skipVote.voted
? await apiDelete<TheatreInfo> (
`/theatres/${ id }/skip_vote`, { params: { post_id: post.id } })
`/theatres/${ id }/skip_vote`, { locale, params: { post_id: post.id } })
: await apiPut<TheatreInfo> (
`/theatres/${ id }/skip_vote`, { post_id: post.id })
`/theatres/${ id }/skip_vote`, { post_id: post.id }, { locale })
applyTheatreInfo (nextInfo)
@@ -586,7 +588,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
catch (error)
{
if (isApiError (error) && error.response?.status === 409)
applyTheatreInfo (await apiPut<TheatreInfo> (`/theatres/${ id }/watching`))
applyTheatreInfo (await apiPut<TheatreInfo> (`/theatres/${ id }/watching`, { }, { locale }))
console.error (error)
}
@@ -600,7 +602,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const handleDelete = async (commentNo: number) => {
try
{
await apiDelete (`/theatres/${ id }/comments/${ commentNo }`)
await apiDelete (`/theatres/${ id }/comments/${ commentNo }`, { locale })
setComments (prev => {
const rtn = [...prev]
const idx = rtn.findIndex (x => x.no === commentNo)
@@ -625,7 +627,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
{
setSending (true)
clearValidationErrors ()
await apiPost (`/theatres/${ id }/comments`, { content })
await apiPost (`/theatres/${ id }/comments`, { content }, { locale })
setContent ('')
commentsRef.current?.scrollTo ({
top: 0,
+7 -6
ファイルの表示
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useBeforeUnload, useLocation, useNavigate } from 'react-router-dom'
import { useBeforeUnload, useLocation } from 'react-router-dom'
import { useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
@@ -251,7 +252,7 @@ const ensureCompleteThemeSlots = (
): UserThemeSlotMap =>
USER_THEME_SLOT_SELECTIONS.reduce<UserThemeSlotMap> (
(complete, selection) => {
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
complete[selection] =
themeSlots[selection]
?? getDefaultUserThemeSlot (
@@ -267,7 +268,7 @@ const comparableThemeTokensForSelection = (
themeSlots: UserThemeSlotMap,
selection: UserThemeSlotSelection,
): ThemeTokens => {
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
const fallback =
getDefaultUserThemeSlot (
baseTheme as BuiltinThemeId,
@@ -647,7 +648,7 @@ const ThemeSection: FC<ThemeSectionProps> = (
const SettingPage: FC<Props> = ({ user, setUser }) => {
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const dialogue = useDialogue ()
const { registerUnsavedChangesSource } = useUnsavedChangesGuard ()
@@ -811,7 +812,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const discardThemeChanges = () => {
const selection = displayedThemeSelection.selection
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
const nextThemeSlots = {
...draftThemeSlots,
[selection]: savedThemeSlots[selection]
@@ -894,7 +895,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
return
const selection = displayedThemeSelection.selection
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
themeSlots = {
...baseState.themeSlots,
[selection]: savedThemeSlots[selection]
+12 -10
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useLocation, useParams } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PostList from '@/components/PostList'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
@@ -24,11 +25,12 @@ import type { Tag } from '@/types'
const WikiDetailPage: FC = () => {
const locale = useLocale ()
const params = useParams ()
const title = params.title ?? ''
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
@@ -37,8 +39,8 @@ const WikiDetailPage: FC = () => {
const { data: wikiPage, isLoading: loading, isSuccess, isFetching, error } = useQuery ({
enabled: Boolean (title) && !(/^\d+$/.test (title)),
queryKey: wikiKeys.show (title, { version }),
queryFn: () => fetchWikiPageByTitle (title, { version }) })
queryKey: wikiKeys.show (title, { version }, locale),
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
const missingWiki = isSuccess && !(isFetching) && wikiPage == null
const { data: titleIndex, isPending: lookupPending,
@@ -63,8 +65,8 @@ const WikiDetailPage: FC = () => {
const { data: tag, isPending: tagPending,
isFetching: tagFetching, error: tagError } = useQuery ({
enabled: Boolean (effectiveTitle),
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => fetchTagByName (effectiveTitle) })
queryKey: tagsKeys.show (effectiveTitle, locale),
queryFn: () => fetchTagByName (effectiveTitle, locale) })
const keys = {
tags: effectiveTitle, match: 'all', page: 1, limit: 8, url: '', title: '',
@@ -73,8 +75,8 @@ const WikiDetailPage: FC = () => {
order: 'original_created_at:desc' } as const
const { data } = useQuery ({
enabled: Boolean (effectiveTitle) && !(version),
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const posts = data?.posts || []
useEffect (() => {
@@ -97,7 +99,7 @@ const WikiDetailPage: FC = () => {
void (async () => {
try
{
const data = await fetchWikiPage (title, { })
const data = await fetchWikiPage (title, { }, locale)
if (active)
navigate (`/wiki/${ encodeURIComponent (data.title) }`, { replace: true })
}
@@ -110,7 +112,7 @@ const WikiDetailPage: FC = () => {
return () => {
active = false
}
}, [title, navigate])
}, [title, navigate, locale])
const displayError = error ?? (missingWiki ? lookupError ?? tagError : null)
if (displayError != null)
+4 -2
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
@@ -14,6 +15,7 @@ import type { WikiPageDiff } from '@/types'
const WikiDiffPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const location = useLocation ()
@@ -29,9 +31,9 @@ const WikiDiffPage: FC = () => {
useEffect (() => {
void (async () => {
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { params: { from, to } }))
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { locale, params: { from, to } }))
}) ()
}, [from, id, to])
}, [from, id, to, locale])
return (
<MainArea>
+8 -6
ファイルの表示
@@ -3,8 +3,9 @@ import MarkdownIt from 'markdown-it'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import MdEditor from 'react-markdown-editor-lite'
import { useParams, useNavigate } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
@@ -36,11 +37,12 @@ type WikiFormField = 'title' | 'body'
const WikiEditPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const { id } = useParams ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const qc = useQueryClient ()
@@ -60,8 +62,8 @@ const WikiEditPage: FC<Props> = ({ user }) => {
try
{
await apiPut (`/wiki/${ id }`, formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }),
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }, locale),
(prev: WikiPage) => ({ ...prev, title, body }))
qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: '投稿成功!' })
@@ -80,12 +82,12 @@ const WikiEditPage: FC<Props> = ({ user }) => {
void (async () => {
setLoading (true)
const data = await apiGet<WikiPage> (`/wiki/${ id }`)
const data = await apiGet<WikiPage> (`/wiki/${ id }`, { locale })
setTitle (data.title)
setBody (data.body)
setLoading (false)
}) ()
}, [editable, id])
}, [editable, id, locale])
if (!(editable))
return <Forbidden/>
+4 -2
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
@@ -15,6 +16,7 @@ import type { WikiPageChange } from '@/types'
const WikiHistoryPage: FC = () => {
const locale = useLocale ()
const [changes, setChanges] = useState<WikiPageChange[]> ([])
const location = useLocation ()
@@ -23,9 +25,9 @@ const WikiHistoryPage: FC = () => {
useEffect (() => {
void (async () => {
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { params: id ? { id } : { } }))
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { locale, params: id ? { id } : { } }))
}) ()
}, [id, location.search])
}, [id, location.search, locale])
return (
<MainArea>
+6 -4
ファイルの表示
@@ -3,9 +3,10 @@ import MarkdownIt from 'markdown-it'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import MdEditor from 'react-markdown-editor-lite'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import 'react-markdown-editor-lite/lib/index.css'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
@@ -35,10 +36,11 @@ type WikiFormField = 'title' | 'body'
const WikiNewPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const qc = useQueryClient ()
const query = new URLSearchParams (location.search)
@@ -59,9 +61,9 @@ const WikiNewPage: FC<Props> = ({ user }) => {
try
{
const data = await apiPost<WikiPage> ('/wiki', formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
await qc.cancelQueries ({ queryKey: wikiKeys.root })
qc.setQueryData (wikiKeys.show (data.title, { }), data)
qc.setQueryData (wikiKeys.show (data.title, { }, locale), data)
await Promise.all ([
qc.invalidateQueries ({ queryKey: wikiKeys.root }),
qc.invalidateQueries ({ queryKey: tagsKeys.root })])
+5 -3
ファイルの表示
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
@@ -15,12 +16,13 @@ import type { WikiPage } from '@/types'
const WikiSearchPage: FC = () => {
const locale = useLocale ()
const [title, setTitle] = useState ('')
const [text, setText] = useState ('')
const [results, setResults] = useState<WikiPage[]> ([])
const search = async () => {
setResults (await apiGet ('/wiki', { params: { title } }))
setResults (await apiGet ('/wiki', { locale, params: { title } }))
}
const handleSearch = (ev: FormEvent) => {
@@ -30,9 +32,9 @@ const WikiSearchPage: FC = () => {
useEffect (() => {
void (async () => {
setResults (await apiGet ('/wiki', { params: { title: '' } }))
setResults (await apiGet ('/wiki', { locale, params: { title: '' } }))
}) ()
}, [])
}, [locale])
return (
<MainArea>
+1 -1
ファイルの表示
@@ -16,7 +16,7 @@ if (typeof originalTransform === 'function')
function (code, id)
{
return (/[?&](raw|url)(?:&|$)/.test (id)
? null
? Promise.resolve (undefined)
: originalTransform.call (this, code, id))
}
}