このコミットが含まれているのは:
2026-10-18 00:20:30 +09:00
コミット 9be0c7b1d9
45個のファイルの変更、355行の追加、199行の削除
+6 -1
ファイルの表示
@@ -8,6 +8,8 @@ const COLUMN_COUNT = 12
const DevModeWatermark: FC = () => { const DevModeWatermark: FC = () => {
const { t } = useTranslation ('shared')
const text = t ('labels.development', { returnDetails: true })
return ( return (
<div <div
aria-hidden="true" aria-hidden="true"
@@ -29,13 +31,14 @@ const DevModeWatermark: FC = () => {
{Array.from ({ length: COLUMN_COUNT }, (_, columnIndex) => ( {Array.from ({ length: COLUMN_COUNT }, (_, columnIndex) => (
<span <span
key={columnIndex} key={columnIndex}
lang={text.usedLng}
className={ className={
'whitespace-nowrap text-3xl font-bold ' 'whitespace-nowrap text-3xl font-bold '
+ 'tracking-[0.3em] text-neutral-950/5 ' + 'tracking-[0.3em] text-neutral-950/5 '
+ 'dark:text-white/10' + 'dark:text-white/10'
} }
style={{ fontFamily: 'Nikumaru' }}> style={{ fontFamily: 'Nikumaru' }}>
開発モード {text.res}
</span>))} </span>))}
</div>))} </div>))}
</div> </div>
@@ -44,3 +47,5 @@ const DevModeWatermark: FC = () => {
export default DevModeWatermark export default DevModeWatermark
import { useTranslation } from 'react-i18next'
+10 -10
ファイルの表示
@@ -4,6 +4,7 @@ import nikumaru from '@/assets/fonts/nikumaru.otf'
import errorImg from '@/assets/images/not-found.gif' import errorImg from '@/assets/images/not-found.gif'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react' import type { FC } from 'react'
@@ -11,17 +12,16 @@ type Props = { status: number }
const ErrorScreen: FC<Props> = ({ status }) => { const ErrorScreen: FC<Props> = ({ status }) => {
const [message, rightMsg, leftMsg]: [string, string, string] = (() => { const { message: translate, label } = useUITranslation ('errors')
const [message]: [string, string, string] = (() => {
switch (status) switch (status)
{ {
case 403: case 403:
return ['権限ないよ(笑)', '帰れ!', '帰れ!']
case 404: case 404:
return ['ページないよ(笑)', '帰れ!', '帰れ!']
case 500: case 500:
return ['鯖でエラー出たって(嘲笑)', '管理人に', '聯絡を!']
case 503: case 503:
return ['鯖死んでるよ(泣)', '管理人に', '聯絡を!'] return [translate (`${ status }.message`).text,
translate (`${ status }.right`).text, translate (`${ status }.left`).text]
default: default:
throw new Error throw new Error
} }
@@ -51,13 +51,13 @@ const ErrorScreen: FC<Props> = ({ status }) => {
style={{ fontFamily: 'Nikumaru' }}> style={{ fontFamily: 'Nikumaru' }}>
<p>{status}</p> <p>{status}</p>
<div className="flex flex-row space-x-1 sm:space-x-2 md:space-x-4"> <div className="flex flex-row space-x-1 sm:space-x-2 md:space-x-4">
<p style={{ writingMode: 'vertical-rl' }}>{leftMsg}</p> <p style={{ writingMode: 'vertical-rl' }}>{label (`${ status }.left`)}</p>
<img className="max-w-[70vw]" src={errorImg} alt="逃げたギター"/> <img className="max-w-[70vw]" src={errorImg} alt={translate ('imageAlt').text}/>
<p style={{ writingMode: 'vertical-rl' }}>{rightMsg}</p> <p style={{ writingMode: 'vertical-rl' }}>{label (`${ status }.right`)}</p>
</div> </div>
<p className="mr-[-.5em]">{message}</p> <p className="mr-[-.5em]">{label (`${ status }.message`)}</p>
</div> </div>
</MainArea>) </MainArea>)
} }
export default ErrorScreen export default ErrorScreen
+3 -1
ファイルの表示
@@ -3,6 +3,7 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import TagSearchBox from '@/components/TagSearchBox' import TagSearchBox from '@/components/TagSearchBox'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import TextArea from '@/components/common/TextArea' import TextArea from '@/components/common/TextArea'
@@ -39,6 +40,7 @@ type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | '
const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => { const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
const { label } = useUITranslation ('navigation')
const locale = useLocale () const locale = useLocale ()
const ref = useRef<HTMLTextAreaElement> (null) const ref = useRef<HTMLTextAreaElement> (null)
@@ -79,7 +81,7 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
} }
return ( return (
<FormField className="relative w-full" label="タグ" messages={errors}> <FormField className="relative w-full" label={label ('tags')} messages={errors}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<> <>
<TextArea <TextArea
+3 -1
ファイルの表示
@@ -3,6 +3,7 @@ import { useRef } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import { useUITranslation } from '@/lib/useUITranslation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' import { useSharedTransitionStore } from '@/stores/sharedTransitionStore'
@@ -16,6 +17,7 @@ type Props = { posts: Post[]
const PostList: FC<Props> = ({ posts, onClick }) => { const PostList: FC<Props> = ({ posts, onClick }) => {
const { label } = useUITranslation ('shared')
const location = useLocation () const location = useLocation ()
const behaviourSettings = useClientBehaviourSettings () const behaviourSettings = useClientBehaviourSettings ()
const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal' const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal'
@@ -75,7 +77,7 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
? ( ? (
<div className="flex h-full w-full items-center justify-center <div className="flex h-full w-full items-center justify-center
bg-muted text-center text-xs text-muted-foreground"> bg-muted text-center text-xs text-muted-foreground">
サムネイルなし {label ('labels.noThumbnail')}
</div>) </div>)
: ( : (
<img <img
+9 -6
ファイルの表示
@@ -1,6 +1,7 @@
import DateTimeField, { toMinutePrecisionIsoUtc } from '@/components/common/DateTimeField' import DateTimeField, { toMinutePrecisionIsoUtc } from '@/components/common/DateTimeField'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react' import type { FC } from 'react'
@@ -19,9 +20,11 @@ const PostOriginalCreatedTimeField: FC<Props> = (
setOriginalCreatedFrom, setOriginalCreatedFrom,
originalCreatedBefore, originalCreatedBefore,
setOriginalCreatedBefore, setOriginalCreatedBefore,
errors }: Props) => { errors }: Props,
) => {
const { label } = useUITranslation ('shared')
return ( return (
<FormField label="オリジナルの作成日時" messages={errors}> <FormField label={label ('labels.originalCreatedAt')} messages={errors}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<> <>
<div className="my-1 flex"> <div className="my-1 flex">
@@ -46,7 +49,7 @@ const PostOriginalCreatedTimeField: FC<Props> = (
d.setMinutes (d.getMinutes () + 1) d.setMinutes (d.getMinutes () + 1)
setOriginalCreatedBefore (toMinutePrecisionIsoUtc (d.toISOString ())) setOriginalCreatedBefore (toMinutePrecisionIsoUtc (d.toISOString ()))
}}/> }}/>
以降 {label ('labels.since')}
</div> </div>
<div> <div>
<Button <Button
@@ -56,7 +59,7 @@ const PostOriginalCreatedTimeField: FC<Props> = (
onClick={() => { onClick={() => {
setOriginalCreatedFrom (null) setOriginalCreatedFrom (null)
}}> }}>
リセット {label ('actions.reset')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -71,7 +74,7 @@ const PostOriginalCreatedTimeField: FC<Props> = (
invalid={invalid} invalid={invalid}
value={originalCreatedBefore ?? undefined} value={originalCreatedBefore ?? undefined}
onChange={setOriginalCreatedBefore}/> onChange={setOriginalCreatedBefore}/>
より前 {label ('labels.before')}
</div> </div>
<div> <div>
<Button <Button
@@ -81,7 +84,7 @@ const PostOriginalCreatedTimeField: FC<Props> = (
onClick={() => { onClick={() => {
setOriginalCreatedBefore (null) setOriginalCreatedBefore (null)
}}> }}>
リセット {label ('actions.reset')}
</Button> </Button>
</div> </div>
</div> </div>
+3 -1
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale' import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { apiGet } from '@/lib/api' import { apiGet } from '@/lib/api'
import { inputClass } from '@/lib/utils' import { inputClass } from '@/lib/utils'
@@ -15,6 +16,7 @@ import type { Tag } from '@/types'
const TagSearch: FC = () => { const TagSearch: FC = () => {
const { message } = useUITranslation ('shared')
const locale = useLocale () const locale = useLocale ()
const location = useLocation () const location = useLocation ()
const navigate = useLocaleNavigate () const navigate = useLocaleNavigate ()
@@ -108,7 +110,7 @@ const TagSearch: FC = () => {
<div className="relative w-full"> <div className="relative w-full">
<input type="text" <input type="text"
data-shortcut-focus-search="true" data-shortcut-focus-search="true"
placeholder="タグ検索..." placeholder={message ('labels.tagSearch').text}
value={search} value={search}
onChange={whenChanged} onChange={whenChanged}
onFocus={() => setSuggestionsVsbl (true)} onFocus={() => setSuggestionsVsbl (true)}
+6 -4
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale' import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import TagLink from '@/components/TagLink' import TagLink from '@/components/TagLink'
import TagSearch from '@/components/TagSearch' import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle' import SectionTitle from '@/components/common/SectionTitle'
@@ -23,6 +24,7 @@ type Props = { posts: Post[]
const TagSidebar: FC<Props> = ({ posts, onClick }) => { const TagSidebar: FC<Props> = ({ posts, onClick }) => {
const { label } = useUITranslation ('shared')
const locale = useLocale () const locale = useLocale ()
const navigate = useLocaleNavigate () const navigate = useLocaleNavigate ()
const behaviourSettings = useClientBehaviourSettings () const behaviourSettings = useClientBehaviourSettings ()
@@ -71,7 +73,7 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
const TagBlock = ( const TagBlock = (
<> <>
<SectionTitle>タグ</SectionTitle> <SectionTitle>{label ('navigation:tags')}</SectionTitle>
<ul> <ul>
{CATEGORIES.flatMap (cat => cat in tags ? ( {CATEGORIES.flatMap (cat => cat in tags ? (
tags[cat].map (tag => ( tags[cat].map (tag => (
@@ -90,7 +92,7 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
</motion.div>)} </motion.div>)}
</li>))) : [])} </li>))) : [])}
</ul> </ul>
<SectionTitle>関聯</SectionTitle> <SectionTitle>{label ('labels.relations')}</SectionTitle>
<a href="#" <a href="#"
onClick={ev => { onClick={ev => {
ev.preventDefault () ev.preventDefault ()
@@ -108,7 +110,7 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
} }
}) ()) }) ())
}}> }}>
ランダム {label ('actions.random')}
</a> </a>
</>) </>)
@@ -148,7 +150,7 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
ev.preventDefault () ev.preventDefault ()
setTagsVsbl (v => !(v)) setTagsVsbl (v => !(v))
}}> }}>
{tagsVsbl ? '▲▲▲ タグ一覧を閉じる ▲▲▲' : '▼▼▼ タグ一覧を表示 ▼▼▼'} {tagsVsbl ? label ('labels.hideTags') : label ('labels.showTags')}
</a> </a>
</SidebarComponent>) </SidebarComponent>)
} }
+12 -11
ファイルの表示
@@ -16,6 +16,7 @@ import { isLocaleCode,
switchLocalePath, switchLocalePath,
unlocalisedPath } from '@/lib/locales' unlocalisedPath } from '@/lib/locales'
import { uiMessage, uiMessageLanguage } from '@/lib/messages' import { uiMessage, uiMessageLanguage } from '@/lib/messages'
import { useUITranslation } from '@/lib/useUITranslation'
import { materialsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys' import { materialsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users' import { canEditContent } from '@/lib/users'
@@ -125,6 +126,7 @@ export const menuOutline = (
const TopNav: FC<Props> = ({ user, locales }) => { const TopNav: FC<Props> = ({ user, locales }) => {
const { label } = useUITranslation ('shared')
const locale = useLocale () const locale = useLocale ()
const localeName = locales.find (entry => entry.code === locale)?.name const localeName = locales.find (entry => entry.code === locale)?.name
const navigate = useLocaleNavigate () const navigate = useLocaleNavigate ()
@@ -206,21 +208,21 @@ const TopNav: FC<Props> = ({ user, locales }) => {
const topNavTransition = const topNavTransition =
animationsOff animationsOff
? { duration: 0 } ? { duration: 0 }
: reducedAnimations : (reducedAnimations
? { duration: .08, ease: 'linear' as const } ? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const } : { duration: .2, ease: 'easeOut' as const })
const opacityTransition = const opacityTransition =
animationsOff animationsOff
? { duration: 0 } ? { duration: 0 }
: reducedAnimations : (reducedAnimations
? { duration: .08 } ? { duration: .08 }
: { duration: .12 } : { duration: .12 })
const highlightTransitionClass = const highlightTransitionClass =
animationsOff animationsOff
? undefined ? undefined
: reducedAnimations : (reducedAnimations
? 'transition-[transform,width] duration-75 ease-linear' ? 'transition-[transform,width] duration-75 ease-linear'
: 'transition-[transform,width] duration-200 ease-out' : 'transition-[transform,width] duration-200 ease-out')
const prevActiveIdxRef = useRef<number> (activeIdx) const prevActiveIdxRef = useRef<number> (activeIdx)
@@ -382,8 +384,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
onClick={() => { onClick={() => {
scroll (0, 0) scroll (0, 0)
}}> }}>
{/* TODO: ここも外出ししろ. */} {label ('labels.brand')}
ぼざクリ タグ広場
</PrefetchLink> </PrefetchLink>
<div ref={navRef} <div ref={navRef}
+3 -1
ファイルの表示
@@ -1,6 +1,7 @@
import Separator from '@/components/MenuSeparator' import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react' import type { FC } from 'react'
@@ -11,6 +12,7 @@ type Props = { user: User | null,
const TopNavUser: FC<Props> = ({ user, sp }) => { const TopNavUser: FC<Props> = ({ user, sp }) => {
const { label } = useUITranslation ('shared')
if (!(user)) if (!(user))
return return
@@ -24,7 +26,7 @@ const TopNavUser: FC<Props> = ({ user, sp }) => {
<> <>
{sp && <Separator/>} {sp && <Separator/>}
<PrefetchLink to="/users/settings" className={className}> <PrefetchLink to="/users/settings" className={className}>
{user.name || '名もなきニジラー'} {user.name || label ('labels.anonymous')}
</PrefetchLink> </PrefetchLink>
</>) </>)
} }
+5 -2
ファイルの表示
@@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown'
import remarkGFM from 'remark-gfm' import remarkGFM from 'remark-gfm'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import { wikiKeys } from '@/lib/queryKeys' import { wikiKeys } from '@/lib/queryKeys'
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink' import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
@@ -27,6 +28,7 @@ const mdComponents = { a: (({ href, children }) => (
const WikiBody: FC<Props> = ({ title, body }) => { const WikiBody: FC<Props> = ({ title, body }) => {
const { message } = useUITranslation ('wiki')
const locale = useLocale () const locale = useLocale ()
const { data } = useQuery ({ const { data } = useQuery ({
enabled: Boolean (body), enabled: Boolean (body),
@@ -39,8 +41,9 @@ const WikiBody: FC<Props> = ({ title, body }) => {
return ( return (
<ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}> <ReactMarkdown components={mdComponents} remarkPlugins={remarkPlugins}>
{body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} {body || message ('missingPage', {
url: `/wiki/new?title=${ encodeURIComponent (title) }` }).text}
</ReactMarkdown>) </ReactMarkdown>)
} }
export default WikiBody export default WikiBody
+3 -2
ファイルの表示
@@ -1,6 +1,7 @@
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
import PostTagsField from '@/components/posts/PostTagsField' import PostTagsField from '@/components/posts/PostTagsField'
import PostTextField from '@/components/posts/PostTextField' import PostTextField from '@/components/posts/PostTextField'
import TranslatedText from '@/components/TranslatedText'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
@@ -54,7 +55,7 @@ const PostCoreDataFields: FC<PostCoreDataFieldsProps> = (
) => ( ) => (
<> <>
<PostTextField <PostTextField
label="タイトル" label={<TranslatedText namespace="shared" messageKey="labels.title"/>}
value={title.value} value={title.value}
disabled={title.disabled} disabled={title.disabled}
warnings={title.warnings} warnings={title.warnings}
@@ -82,7 +83,7 @@ const PostCoreDataFields: FC<PostCoreDataFieldsProps> = (
rows={tags.rows}/> rows={tags.rows}/>
<PostTextField <PostTextField
label="親投稿" label={<TranslatedText namespace="shared" messageKey="labels.parentPosts"/>}
value={parentPostIds.value} value={parentPostIds.value}
disabled={parentPostIds.disabled} disabled={parentPostIds.disabled}
warnings={parentPostIds.warnings} warnings={parentPostIds.warnings}
+3 -3
ファイルの表示
@@ -1,4 +1,5 @@
import PostTextField from '@/components/posts/PostTextField' import PostTextField from '@/components/posts/PostTextField'
import TranslatedText from '@/components/TranslatedText'
import type { FC } from 'react' import type { FC } from 'react'
@@ -16,12 +17,11 @@ const PostDurationField: FC<Props> = (
disabled }, disabled },
) => ( ) => (
<PostTextField <PostTextField
label="動画時間" label={<TranslatedText namespace="shared" messageKey="labels.duration"/>}
value={value} value={value}
onChange={onChange} onChange={onChange}
errors={errors} errors={errors}
disabled={disabled} disabled={disabled}
type="text"/> type="text"/>)
)
export default PostDurationField export default PostDurationField
+1 -1
ファイルの表示
@@ -5,7 +5,7 @@ import { inputClass } from '@/lib/utils'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
type Props = { type Props = {
label: string label: ReactNode
value: string value: string
onChange: (value: string) => void onChange: (value: string) => void
warnings?: string[] warnings?: string[]
+4 -2
ファイルの表示
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react' import type { FC } from 'react'
@@ -15,10 +16,11 @@ type Props = {
const PostThumbnailPreview: FC<Props> = ( const PostThumbnailPreview: FC<Props> = (
{ url, { url,
file, file,
alt = 'サムネール', alt,
className = 'h-16 w-16', className = 'h-16 w-16',
referrerPolicy }, referrerPolicy },
) => { ) => {
const { message } = useUITranslation ('shared')
const [failed, setFailed] = useState (false) const [failed, setFailed] = useState (false)
const [fileUrl, setFileUrl] = useState<string | null> (null) const [fileUrl, setFileUrl] = useState<string | null> (null)
@@ -55,7 +57,7 @@ const PostThumbnailPreview: FC<Props> = (
return ( return (
<img <img
src={resolvedUrl} src={resolvedUrl}
alt={alt} alt={alt ?? message ('labels.thumbnail').text}
referrerPolicy={referrerPolicy} referrerPolicy={referrerPolicy}
className={cn (className, 'rounded border border-border object-cover')} className={cn (className, 'rounded border border-border object-cover')}
onError={() => setFailed (true)}/>) onError={() => setFailed (true)}/>)
+2 -9
ファイルの表示
@@ -1,4 +1,5 @@
import StatusBadge from '@/components/common/StatusBadge' import StatusBadge from '@/components/common/StatusBadge'
import TranslatedText from '@/components/TranslatedText'
import type { FC } from 'react' import type { FC } from 'react'
@@ -8,14 +9,6 @@ import type { PostImportBadgeValue } from '@/components/posts/import/postImportR
type Props = { type Props = {
value: PostImportBadgeValue } value: PostImportBadgeValue }
const LABELS: Record<PostImportBadgeValue, string> = {
ready: '登録可能',
error: '登録不可',
warning: '警告',
skipped: 'スキップ',
created: '登録済み',
failed: '登録失敗' }
const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = { const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
ready: 'success', ready: 'success',
error: 'warning', error: 'warning',
@@ -27,7 +20,7 @@ const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
const PostImportStatusBadge: FC<Props> = ({ value }) => ( const PostImportStatusBadge: FC<Props> = ({ value }) => (
<StatusBadge tone={TONES[value]}> <StatusBadge tone={TONES[value]}>
{LABELS[value]} <TranslatedText namespace="imports" messageKey={`status.${ value }`}/>
</StatusBadge>) </StatusBadge>)
export default PostImportStatusBadge export default PostImportStatusBadge
+1 -1
ファイルの表示
@@ -12,7 +12,7 @@ type Props = {
const PostImportThumbnailPreview: FC<Props> = ( const PostImportThumbnailPreview: FC<Props> = (
{ url, { url,
file, file,
alt = 'サムネール', alt,
className = 'h-16 w-16' }, className = 'h-16 w-16' },
) => ( ) => (
<PostThumbnailPreview <PostThumbnailPreview
+18 -15
ファイルの表示
@@ -1,13 +1,6 @@
import { createInstance } from 'i18next' import { createInstance } from 'i18next'
import { initReactI18next } from 'react-i18next' import { initReactI18next } from 'react-i18next'
import ainuNavigation from '@/locales/ain-Kana/navigation.json'
import ainuCommon from '@/locales/ain-Kana/common.json'
import ainuTags from '@/locales/ain-Kana/tags.json'
import nipponeseNavigation from '@/locales/ja/navigation.json'
import nipponeseCommon from '@/locales/ja/common.json'
import nipponeseTags from '@/locales/ja/tags.json'
import type { LocaleCode } from '@/lib/locales' import type { LocaleCode } from '@/lib/locales'
export const i18n = createInstance () export const i18n = createInstance ()
@@ -17,6 +10,8 @@ export const i18n = createInstance ()
const translatedResource = (resource: Record<string, unknown>): Record<string, unknown> => { const translatedResource = (resource: Record<string, unknown>): Record<string, unknown> => {
const untranslated = new Set ( const untranslated = new Set (
Array.isArray (resource._untranslated) ? resource._untranslated : []) Array.isArray (resource._untranslated) ? resource._untranslated : [])
if (untranslated.has ('*'))
return { }
const select = (entries: Record<string, unknown>, prefix = ''): Record<string, unknown> => const select = (entries: Record<string, unknown>, prefix = ''): Record<string, unknown> =>
Object.fromEntries (Object.entries (entries).flatMap (([key, value]) => { Object.fromEntries (Object.entries (entries).flatMap (([key, value]) => {
const path = prefix ? `${ prefix }.${ key }` : key const path = prefix ? `${ prefix }.${ key }` : key
@@ -32,6 +27,19 @@ const translatedResource = (resource: Record<string, unknown>): Record<string, u
} }
const dictionaries = import.meta.glob<Record<string, unknown>> ('../locales/*/*.json', {
eager: true, import: 'default' })
const resources: Record<string, Record<string, Record<string, unknown>>> = { }
for (const [path, dictionary] of Object.entries (dictionaries))
{
const [, , locale, filename] = path.split ('/')
resources[locale] ??= { }
resources[locale][filename.slice (0, -5)] =
locale === 'ain-Kana' ? translatedResource (dictionary) : dictionary
}
const namespaces = Object.keys (resources.ja ?? { })
void i18n.use (initReactI18next).init ({ void i18n.use (initReactI18next).init ({
lng: 'ja', lng: 'ja',
fallbackLng: 'ja', fallbackLng: 'ja',
@@ -39,13 +47,8 @@ void i18n.use (initReactI18next).init ({
load: 'currentOnly', load: 'currentOnly',
initAsync: false, initAsync: false,
defaultNS: 'navigation', defaultNS: 'navigation',
ns: ['navigation', 'common', 'tags'], ns: namespaces,
resources: { resources,
ja: { navigation: nipponeseNavigation, common: nipponeseCommon, tags: nipponeseTags },
'ain-Kana': {
navigation: ainuNavigation,
common: translatedResource (ainuCommon),
tags: translatedResource (ainuTags) } },
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
returnNull: false, returnNull: false,
returnEmptyString: false, returnEmptyString: false,
@@ -77,7 +80,7 @@ export const untranslatedKeys = (
if (import.meta.env.DEV) if (import.meta.env.DEV)
{ {
for (const namespace of ['navigation', 'common', 'tags']) for (const namespace of namespaces)
{ {
const missing = untranslatedKeys ('ain-Kana', namespace) const missing = untranslatedKeys ('ain-Kana', namespace)
if (missing.length > 0) if (missing.length > 0)
+9 -1
ファイルの表示
@@ -1,5 +1,7 @@
import { createElement } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LocalisedText from '@/components/LocalisedText'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
@@ -12,5 +14,11 @@ export const useUITranslation = (namespace = 'common') => {
return { text: result.res, language: result.usedLng } return { text: result.res, language: result.usedLng }
} }
return { message } const label = (key: string, values?: Record<string, string | number>) => {
const result = message (key, values)
return createElement (LocalisedText, {
language: result.language, children: result.text })
}
return { message, label }
} }
+22 -37
ファイルの表示
@@ -1,38 +1,23 @@
{ { "countLabel": "チピㇱキㇷ゚",
"_untranslated": [ "categoryLabel": "イソネカ",
"countLabel", "categoryLabel", "stateLabel", "loading", "fetchError", "stateLabel": "シㇼキ",
"states", "categories.deerjikist", "categories.meme", "categories.character",
"categories.general", "categories.meta", "categories.nico", "pagination", "tagLink"
],
"countLabel": "件数",
"categoryLabel": "カテゴリ",
"stateLabel": "状態",
"search": "イフナラ", "search": "イフナラ",
"loading": "Loading...", "loading": "タツ゚ㇺ アウㇰ コㇿ アン…",
"fetchError": "データ取得エラー", "fetchError": "タツ゚ㇺ アウㇰ エアイカㇷ゚",
"states": { "states": { "active": "アㇱカイ",
"active": "有効", "deprecated": "タネ ソモ アエイワンケ" },
"deprecated": "廃止" "categories": { "deerjikist": "ユㇰ ニチカ ウタㇻ",
}, "meme": "MEME",
"categories": { "character": "イノカ",
"deerjikist": "ニジラー", "general": "ウサ オルㇱペ",
"meme": "原作・ネタ元・ミーム等", "material": "チカㇻペ",
"character": "キャラクター", "meta": "タㇰ オルㇱペ タㇰ",
"general": "一般", "nico": "ニコニコ タㇰ" },
"material": "チカㇻペ", "pagination": { "label": "ソㇱ",
"meta": "メタタグ", "first": "イヨッタ ホㇱキ ソㇱ",
"nico": "ニコニコタグ" "previous": "ホㇱキ ソㇱ",
}, "next": "トゥタヌ ソㇱ",
"pagination": { "last": "イヨッタ イヨㇱノ アン ソㇱ" },
"label": "Pagination", "tagLink": { "missingMaterial": "{{name}}:チカㇻペ アスㇽソㇱ ソモ アン。",
"first": "最初のページ", "missingDeerjikist": "{{name}}:アスㇽソㇱ ソモ アン。",
"previous": "前のページ", "missingWiki": "{{name}}:Wiki ソモ アン。" } }
"next": "次のページ",
"last": "最後のページ"
},
"tagLink": {
"missingMaterial": "{{name}} 素材情報が存在しません.",
"missingDeerjikist": "{{name}} に関する情報が存在しません.",
"missingWiki": "{{name}} Wiki が存在しません."
}
}
+8
ファイルの表示
@@ -0,0 +1,8 @@
{
"_untranslated": ["*"],
"403": { "message": "権限ないよ(笑)", "right": "帰れ!", "left": "帰れ!" },
"404": { "message": "ページないよ(笑)", "right": "帰れ!", "left": "帰れ!" },
"500": { "message": "鯖でエラー出たって(嘲笑)", "right": "管理人に", "left": "聯絡を!" },
"503": { "message": "鯖死んでるよ(泣)", "right": "管理人に", "left": "聯絡を!" },
"imageAlt": "逃げたギター"
}
+6
ファイルの表示
@@ -0,0 +1,6 @@
{
"_untranslated": ["*"],
"title": "ヘルプ", "search": "ヘルプ検索", "history": "ヘルプ改定履歴",
"effectiveSince": "有効開始年月日", "empty": "該当する記事がありません.",
"diffTitle": "ヘルプ 差分: {{title}}", "articleTitle": "{{title}} ヘルプ"
}
+7
ファイルの表示
@@ -0,0 +1,7 @@
{
"_untranslated": ["*"],
"status": {
"ready": "登録可能", "error": "登録不可", "warning": "警告",
"skipped": "スキップ", "created": "登録済み", "failed": "登録失敗"
}
}
+1 -1
ファイルの表示
@@ -7,7 +7,7 @@
"allHistory": "フㇱコアンペ オピッタ", "allHistory": "フㇱコアンペ オピッタ",
"help": "ヤイカオパㇱ", "help": "ヤイカオパㇱ",
"tags": "タㇰ", "tags": "タㇰ",
"master": "タㇰ オピッタ", "master": "オピッタ",
"nicoLinks": "ニコニコ ウコテㇷ゚", "nicoLinks": "ニコニコ ウコテㇷ゚",
"materials": "チカㇻペ", "materials": "チカㇻペ",
"suppressions": "アエアイカㇷ゚テㇷ゚", "suppressions": "アエアイカㇷ゚テㇷ゚",
+30
ファイルの表示
@@ -0,0 +1,30 @@
{
"_untranslated": ["*"],
"labels": {
"title": "タイトル", "content": "内容", "body": "本文",
"lastUpdated": "最終更新", "thumbnail": "サムネール",
"noThumbnail": "サムネイルなし", "thumbnailMissing": "サムネールなし",
"parentPosts": "親投稿", "duration": "動画時間", "platform": "プラットフォーム",
"code": "コード", "information": "情報", "relations": "関聯",
"uploadedBy": "耕作者: ", "uploadedAt": "耕作日時: ", "link": "リンク: ",
"originalPostedAt": "オリジナルの投稿日時: ", "originalCreatedAt": "オリジナルの作成日時",
"brand": "ぼざクリ タグ広場", "since": "以降", "before": "より前",
"tagSearch": "タグ検索...", "hideTags": "▲▲▲ タグ一覧を閉じる ▲▲▲",
"showTags": "▼▼▼ タグ一覧を表示 ▼▼▼",
"resultsCount": "{{count}} 件", "menu": "メニュー", "anonymous": "名もなきニジラー",
"bot": "bot操作", "deprecatedSuffix": "(廃止)", "development": "開発モード"
},
"actions": {
"update": "更新", "edit": "編輯", "add": "追加", "reset": "リセット",
"cancel": "取消", "retry": "再試行", "skip": "スキップ", "random": "ランダム",
"old": "< 古", "new": "新 >", "oldest": "(最古)", "latest": "(最新)",
"diff": "差分", "restore": "復元", "show": "表示"
},
"messages": {
"loading": "読み込み中...", "loadingEllipsis": "読込中……",
"updated": "更新しました.", "updateFailed": "更新失敗", "failed": "失敗……",
"checkInput": "入力を確認してください.", "checkInputModern": "入力を確認してください。",
"checkInputContent": "入力内容を確認してください.", "reviewInput": "入力を見直してください.",
"postSuccess": "投稿成功!", "postFailed": "投稿失敗", "resetChanges": "変更をリセットしますか?"
}
}
+3 -7
ファイルの表示
@@ -1,8 +1,5 @@
{ { "_untranslated": ["name", "postCount", "firstRecordedAt", "definitionUpdatedAt", "aliases",
"_untranslated": [ "parents", "firstRecordedColumn", "updatedAt", "empty"],
"name", "postCount", "firstRecordedAt", "definitionUpdatedAt", "aliases",
"parents", "firstRecordedColumn", "updatedAt", "empty"
],
"title": "タㇰ", "title": "タㇰ",
"name": "名前", "name": "名前",
"postCount": "広場の投稿数", "postCount": "広場の投稿数",
@@ -13,5 +10,4 @@
"firstRecordedColumn": "最初の記載日時", "firstRecordedColumn": "最初の記載日時",
"updatedAt": "更新日時", "updatedAt": "更新日時",
"history": "フㇱコアンペ", "history": "フㇱコアンペ",
"empty": "結果ないよ(笑)" "empty": "結果ないよ(笑)" }
}
+9
ファイルの表示
@@ -0,0 +1,9 @@
{
"_untranslated": ["*"],
"newPage": "新規 Wiki ページ",
"editPage": "Wiki ページを編輯",
"history": "Wiki 変更履歴",
"historyHeading": "Wiki 履歴", "change": "変更", "dateTime": "日時",
"diffTitle": "Wiki 差分: {{title}}",
"missingPage": "このページは存在しません。[新規作成してください]({{url}})。"
}
+7
ファイルの表示
@@ -0,0 +1,7 @@
{
"403": { "message": "権限ないよ(笑)", "right": "帰れ!", "left": "帰れ!" },
"404": { "message": "ページないよ(笑)", "right": "帰れ!", "left": "帰れ!" },
"500": { "message": "鯖でエラー出たって(嘲笑)", "right": "管理人に", "left": "聯絡を!" },
"503": { "message": "鯖死んでるよ(泣)", "right": "管理人に", "left": "聯絡を!" },
"imageAlt": "逃げたギター"
}
+5
ファイルの表示
@@ -0,0 +1,5 @@
{
"title": "ヘルプ", "search": "ヘルプ検索", "history": "ヘルプ改定履歴",
"effectiveSince": "有効開始年月日", "empty": "該当する記事がありません.",
"diffTitle": "ヘルプ 差分: {{title}}", "articleTitle": "{{title}} ヘルプ"
}
+6
ファイルの表示
@@ -0,0 +1,6 @@
{
"status": {
"ready": "登録可能", "error": "登録不可", "warning": "警告",
"skipped": "スキップ", "created": "登録済み", "failed": "登録失敗"
}
}
+29
ファイルの表示
@@ -0,0 +1,29 @@
{
"labels": {
"title": "タイトル", "content": "内容", "body": "本文",
"lastUpdated": "最終更新", "thumbnail": "サムネール",
"noThumbnail": "サムネイルなし", "thumbnailMissing": "サムネールなし",
"parentPosts": "親投稿", "duration": "動画時間", "platform": "プラットフォーム",
"code": "コード", "information": "情報", "relations": "関聯",
"uploadedBy": "耕作者: ", "uploadedAt": "耕作日時: ", "link": "リンク: ",
"originalPostedAt": "オリジナルの投稿日時: ", "originalCreatedAt": "オリジナルの作成日時",
"brand": "ぼざクリ タグ広場", "since": "以降", "before": "より前",
"tagSearch": "タグ検索...", "hideTags": "▲▲▲ タグ一覧を閉じる ▲▲▲",
"showTags": "▼▼▼ タグ一覧を表示 ▼▼▼",
"resultsCount": "{{count}} 件", "menu": "メニュー", "anonymous": "名もなきニジラー",
"bot": "bot操作", "deprecatedSuffix": "(廃止)", "development": "開発モード"
},
"actions": {
"update": "更新", "edit": "編輯", "add": "追加", "reset": "リセット",
"cancel": "取消", "retry": "再試行", "skip": "スキップ", "random": "ランダム",
"old": "< 古", "new": "新 >", "oldest": "(最古)", "latest": "(最新)",
"diff": "差分", "restore": "復元", "show": "表示"
},
"messages": {
"loading": "読み込み中...", "loadingEllipsis": "読込中……",
"updated": "更新しました.", "updateFailed": "更新失敗", "failed": "失敗……",
"checkInput": "入力を確認してください.", "checkInputModern": "入力を確認してください。",
"checkInputContent": "入力内容を確認してください.", "reviewInput": "入力を見直してください.",
"postSuccess": "投稿成功!", "postFailed": "投稿失敗", "resetChanges": "変更をリセットしますか?"
}
}
+2 -4
ファイルの表示
@@ -1,5 +1,4 @@
{ { "title": "タグ",
"title": "タグ",
"name": "名前", "name": "名前",
"postCount": "広場の投稿数", "postCount": "広場の投稿数",
"firstRecordedAt": "はじめて記載された日時", "firstRecordedAt": "はじめて記載された日時",
@@ -9,5 +8,4 @@
"firstRecordedColumn": "最初の記載日時", "firstRecordedColumn": "最初の記載日時",
"updatedAt": "更新日時", "updatedAt": "更新日時",
"history": "耕作履歴", "history": "耕作履歴",
"empty": "結果ないよ(笑)" "empty": "結果ないよ(笑)" }
}
+8
ファイルの表示
@@ -0,0 +1,8 @@
{
"newPage": "新規 Wiki ページ",
"editPage": "Wiki ページを編輯",
"history": "Wiki 変更履歴",
"historyHeading": "Wiki 履歴", "change": "変更", "dateTime": "日時",
"diffTitle": "Wiki 差分: {{title}}",
"missingPage": "このページは存在しません。[新規作成してください]({{url}})。"
}
+3 -1
ファイルの表示
@@ -6,6 +6,7 @@ import { menuOutline } from '@/components/TopNav'
import SectionTitle from '@/components/common/SectionTitle' import SectionTitle from '@/components/common/SectionTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react' import type { FC } from 'react'
@@ -13,13 +14,14 @@ import type { User } from '@/types'
const MorePage: FC = () => { const MorePage: FC = () => {
const { message } = useUITranslation ('shared')
const menu = menuOutline ( const menu = menuOutline (
{ tag: null, wikiId: null, user: { } as User, pathName: location.pathname }) { tag: null, wikiId: null, user: { } as User, pathName: location.pathname })
return ( return (
<MainArea className="md:flex"> <MainArea className="md:flex">
<Helmet> <Helmet>
<title>{`メニュー | ${ SITE_TITLE }`}</title> <title>{message ('labels.menu').text} | {SITE_TITLE}</title>
</Helmet> </Helmet>
{[...Array (Math.ceil (menu.length / 4)).keys ()].map (i => ( {[...Array (Math.ceil (menu.length / 4)).keys ()].map (i => (
+8 -5
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import TagLink from '@/components/TagLink' import TagLink from '@/components/TagLink'
import FieldError from '@/components/common/FieldError' import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
@@ -25,6 +26,7 @@ type DeerjikistFormField =
const DeerjikistDetailPage: FC = () => { const DeerjikistDetailPage: FC = () => {
const { label } = useUITranslation ('shared')
const locale = useLocale () const locale = useLocale ()
const { id } = useParams () const { id } = useParams ()
const tagId = String (id ?? '') const tagId = String (id ?? '')
@@ -54,12 +56,13 @@ const DeerjikistDetailPage: FC = () => {
setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data, { locale })) setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data, { locale }))
qc.invalidateQueries ({ queryKey: tagsKeys.root }) qc.invalidateQueries ({ queryKey: tagsKeys.root })
toast ({ description: '更新しました.' }) toast ({ description: label ('messages.updated') })
} }
catch (e) catch (e)
{ {
applyValidationError (e) applyValidationError (e)
toast ({ title: '更新失敗', description: '入力内容を確認してください.' }) toast ({ title: label ('messages.updateFailed'),
description: label ('messages.checkInputContent') })
} }
finally finally
{ {
@@ -106,7 +109,7 @@ const DeerjikistDetailPage: FC = () => {
{/* プラットフォーム */} {/* プラットフォーム */}
<FormField <FormField
label="プラットフォーム" label={label ('labels.platform')}
messages={fieldErrors[`deerjikists${ i }Platform`]}> messages={fieldErrors[`deerjikists${ i }Platform`]}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<select <select
@@ -131,7 +134,7 @@ const DeerjikistDetailPage: FC = () => {
{/* コード */} {/* コード */}
<FormField <FormField
label="コード" label={label ('labels.code')}
messages={fieldErrors[`deerjikists${ i }Code`]}> messages={fieldErrors[`deerjikists${ i }Code`]}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<input <input
@@ -166,7 +169,7 @@ const DeerjikistDetailPage: FC = () => {
(disabled (disabled
? 'text-gray-300 bg-gray-500' ? 'text-gray-300 bg-gray-500'
: 'text-white bg-blue-500'))}> : 'text-white bg-blue-500'))}>
更新 {label ('actions.update')}
</button> </button>
</div> </div>
</form> </form>
+10 -8
ファイルの表示
@@ -11,6 +11,7 @@ import { SITE_TITLE } from '@/config'
import { getHelpChanges, helpEditionDate } from '@/lib/help' import { getHelpChanges, helpEditionDate } from '@/lib/help'
import { localeLanguage } from '@/lib/locales' import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { helpKeys } from '@/lib/queryKeys' import { helpKeys } from '@/lib/queryKeys'
import { LIST_LIMIT_OPTIONS } from '@/lib/settings' import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
import { dateString } from '@/lib/utils' import { dateString } from '@/lib/utils'
@@ -19,6 +20,7 @@ import type { FC } from 'react'
const HelpChangesPage: FC = () => { const HelpChangesPage: FC = () => {
const { message, label } = useUITranslation ('help')
const language = localeLanguage (useLocale ()) const language = localeLanguage (useLocale ())
const [searchParams, setSearchParams] = useSearchParams () const [searchParams, setSearchParams] = useSearchParams ()
const identifier = searchParams.get ('identifier') ?? '' const identifier = searchParams.get ('identifier') ?? ''
@@ -51,12 +53,12 @@ const HelpChangesPage: FC = () => {
return ( return (
<MainArea className="min-w-0 max-w-full"> <MainArea className="min-w-0 max-w-full">
<Helmet> <Helmet>
<title>{`ヘルプ改定履歴 | ${ SITE_TITLE }`}</title> <title>{message ('history').text} | {SITE_TITLE}</title>
</Helmet> </Helmet>
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<PageTitle>ヘルプ改定履歴</PageTitle> <PageTitle>{label ('history')}</PageTitle>
<label className="flex items-center gap-2 text-xs text-muted-foreground"> <label className="flex items-center gap-2 text-xs text-muted-foreground">
<span>件数</span> <span>{label ('common:countLabel')}</span>
<select value={limit} onChange={ev => handleLimit (ev.target.value)} <select value={limit} onChange={ev => handleLimit (ev.target.value)}
className="rounded border border-border bg-background px-2 py-1 text-sm className="rounded border border-border bg-background px-2 py-1 text-sm
text-foreground"> text-foreground">
@@ -67,7 +69,7 @@ const HelpChangesPage: FC = () => {
</div> </div>
{isLoading {isLoading
? <p className="mt-4">読み込み中...</p> ? <p className="mt-4">{label ('shared:messages.loading')}</p>
: (error : (error
? <p className="mt-4 text-red-600" role="alert">{error.message}</p> ? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
: ( : (
@@ -77,9 +79,9 @@ const HelpChangesPage: FC = () => {
border-collapse"> border-collapse">
<thead className="border-b-2 border-black dark:border-white"> <thead className="border-b-2 border-black dark:border-white">
<tr> <tr>
<th className="p-2 text-left">差分</th> <th className="p-2 text-left">{label ('shared:actions.diff')}</th>
<th className="p-2 text-left">タイトル</th> <th className="p-2 text-left">{label ('shared:labels.title')}</th>
<th className="p-2 text-left">有効開始年月日</th> <th className="p-2 text-left">{label ('effectiveSince')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -97,7 +99,7 @@ const HelpChangesPage: FC = () => {
<td className="p-2"> <td className="p-2">
{entry.pred != null && ( {entry.pred != null && (
<PrefetchLink to={diffTo}> <PrefetchLink to={diffTo}>
差分 {label ('shared:actions.diff')}
</PrefetchLink>)} </PrefetchLink>)}
</td> </td>
<td className="p-2"> <td className="p-2">
+10 -6
ファイルの表示
@@ -12,6 +12,7 @@ import { getHelpArticle,
resolveHelpLanguage } from '@/lib/help' resolveHelpLanguage } from '@/lib/help'
import { localeLanguage } from '@/lib/locales' import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { dateString } from '@/lib/utils' import { dateString } from '@/lib/utils'
import { helpKeys } from '@/lib/queryKeys' import { helpKeys } from '@/lib/queryKeys'
import NotFound from '@/pages/NotFound' import NotFound from '@/pages/NotFound'
@@ -20,6 +21,7 @@ import type { FC } from 'react'
const HelpDetailPage: FC = () => { const HelpDetailPage: FC = () => {
const { message, label } = useUITranslation ('help')
const locale = useLocale () const locale = useLocale ()
const { identifier = '' } = useParams () const { identifier = '' } = useParams ()
const location = useLocation () const location = useLocation ()
@@ -71,7 +73,9 @@ const HelpDetailPage: FC = () => {
return ( return (
<MainArea> <MainArea>
<Helmet> <Helmet>
<title>{current?.title || identifier} ヘルプ | {SITE_TITLE}</title> <title>
{message ('articleTitle', { title: current?.title || identifier }).text} | {SITE_TITLE}
</title>
</Helmet> </Helmet>
{requestedEdition && ( {requestedEdition && (
@@ -81,9 +85,9 @@ const HelpDetailPage: FC = () => {
? ( ? (
<PrefetchLink <PrefetchLink
to={`/help/${ encodeURIComponent (identifier) }?edition=${ pred }`}> to={`/help/${ encodeURIComponent (identifier) }?edition=${ pred }`}>
&lt; 古 {label ('shared:actions.old')}
</PrefetchLink>) </PrefetchLink>)
: '(最古)'} : label ('shared:actions.oldest')}
<span> <span>
{editionDate != null && dateString (editionDate, 'hour')} {editionDate != null && dateString (editionDate, 'hour')}
@@ -94,13 +98,13 @@ const HelpDetailPage: FC = () => {
? ( ? (
<PrefetchLink <PrefetchLink
to={`/help/${ encodeURIComponent (identifier) }?edition=${ succ }`}> to={`/help/${ encodeURIComponent (identifier) }?edition=${ succ }`}>
新 &gt; {label ('shared:actions.new')}
</PrefetchLink>) </PrefetchLink>)
: '(最新)'} : label ('shared:actions.latest')}
</div>)} </div>)}
<article lang={language} 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>} {Content ? <Content/> : <div>{label ('common:loading')}</div>}
</article> </article>
</MainArea>) </MainArea>)
} }
+3 -1
ファイルの表示
@@ -9,6 +9,7 @@ import { SITE_TITLE } from '@/config'
import { getHelpArticle, getHelpDiff, resolveHelpLanguage } from '@/lib/help' import { getHelpArticle, getHelpDiff, resolveHelpLanguage } from '@/lib/help'
import { localeLanguage } from '@/lib/locales' import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { helpKeys } from '@/lib/queryKeys' import { helpKeys } from '@/lib/queryKeys'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import NotFound from '@/pages/NotFound' import NotFound from '@/pages/NotFound'
@@ -19,6 +20,7 @@ const markers = { added: '+ ', removed: '- ', unchanged: ' ' }
const HelpDiffPage: FC = () => { const HelpDiffPage: FC = () => {
const { message } = useUITranslation ('help')
const locale = useLocale () const locale = useLocale ()
const { identifier = '' } = useParams () const { identifier = '' } = useParams ()
const [searchParams] = useSearchParams () const [searchParams] = useSearchParams ()
@@ -47,7 +49,7 @@ const HelpDiffPage: FC = () => {
return ( return (
<MainArea className="min-w-0 max-w-full"> <MainArea className="min-w-0 max-w-full">
<Helmet> <Helmet>
<title>{`ヘルプ 差分: ${ title } | ${ SITE_TITLE }`}</title> <title>{message ('diffTitle', { title }).text} | {SITE_TITLE}</title>
</Helmet> </Helmet>
<PageTitle>{title}</PageTitle> <PageTitle>{title}</PageTitle>
+17 -13
ファイルの表示
@@ -17,6 +17,7 @@ import { compareHelpEditions,
normaliseHelpSearchText } from '@/lib/help' normaliseHelpSearchText } from '@/lib/help'
import { localeLanguage } from '@/lib/locales' import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import { helpKeys } from '@/lib/queryKeys' import { helpKeys } from '@/lib/queryKeys'
import { LIST_LIMIT_OPTIONS } from '@/lib/settings' import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
import { dateString, inputClass } from '@/lib/utils' import { dateString, inputClass } from '@/lib/utils'
@@ -32,6 +33,7 @@ const defaultDirection: Record<HelpOrderField, 'asc' | 'desc'> = {
const HelpSearchPage: FC = () => { const HelpSearchPage: FC = () => {
const { message, label } = useUITranslation ('help')
const language = localeLanguage (useLocale ()) const language = localeLanguage (useLocale ())
const [searchParams, setSearchParams] = useSearchParams () const [searchParams, setSearchParams] = useSearchParams ()
const pageValue = searchParams.get ('page') ?? '1' const pageValue = searchParams.get ('page') ?? '1'
@@ -111,14 +113,14 @@ const HelpSearchPage: FC = () => {
return ( return (
<MainArea className="min-w-0 max-w-full"> <MainArea className="min-w-0 max-w-full">
<Helmet> <Helmet>
<title>{`ヘルプ | ${ SITE_TITLE }`}</title> <title>{message ('title').text} | {SITE_TITLE}</title>
</Helmet> </Helmet>
<div className="max-w-xl"> <div className="max-w-xl">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<PageTitle>ヘルプ検索</PageTitle> <PageTitle>{label ('search')}</PageTitle>
<label className="flex items-center gap-2 text-xs text-muted-foreground"> <label className="flex items-center gap-2 text-xs text-muted-foreground">
<span>件数</span> <span>{label ('common:countLabel')}</span>
<select value={limit} onChange={ev => handleLimit (ev.target.value)} <select value={limit} onChange={ev => handleLimit (ev.target.value)}
className="rounded border border-border bg-background px-2 py-1 text-sm className="rounded border border-border bg-background px-2 py-1 text-sm
text-foreground"> text-foreground">
@@ -128,19 +130,19 @@ const HelpSearchPage: FC = () => {
</label> </label>
</div> </div>
<form onSubmit={handleSearch} className="space-y-2"> <form onSubmit={handleSearch} className="space-y-2">
<FormField label="タイトル"> <FormField label={label ('shared:labels.title')}>
{({ invalid }) => ( {({ invalid }) => (
<input type="text" <input type="text"
aria-label="タイトル" aria-label={message ('shared:labels.title').text}
value={title} value={title}
onChange={e => setTitle (e.target.value)} onChange={e => setTitle (e.target.value)}
className={inputClass (invalid)}/>)} className={inputClass (invalid)}/>)}
</FormField> </FormField>
<FormField label="内容"> <FormField label={label ('shared:labels.content')}>
{({ invalid }) => ( {({ invalid }) => (
<input type="text" <input type="text"
aria-label="内容" aria-label={message ('shared:labels.content').text}
value={text} value={text}
onChange={e => setText (e.target.value)} onChange={e => setText (e.target.value)}
className={inputClass (invalid)}/>)} className={inputClass (invalid)}/>)}
@@ -149,19 +151,21 @@ const HelpSearchPage: FC = () => {
<div className="py-3"> <div className="py-3">
<button type="submit" <button type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"> className="bg-blue-500 text-white px-4 py-2 rounded">
検索 {label ('common:search')}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
{loading {loading
? <p className="mt-4">読み込み中...</p> ? <p className="mt-4">{label ('shared:messages.loading')}</p>
: (error : (error
? <p className="mt-4 text-red-600" role="alert">{error.message}</p> ? <p className="mt-4 text-red-600" role="alert">{error.message}</p>
: ( : (
<div className="mt-4"> <div className="mt-4">
<p className="mb-2 text-sm" aria-live="polite">{results.length} 件</p> <p className="mb-2 text-sm" aria-live="polite">
{label ('shared:labels.resultsCount', { count: results.length })}
</p>
<div className="min-w-0 max-w-full overflow-x-auto"> <div className="min-w-0 max-w-full overflow-x-auto">
<table className="table-auto w-full min-w-[640px] whitespace-nowrap <table className="table-auto w-full min-w-[640px] whitespace-nowrap
border-collapse text-gray-900 dark:text-gray-100"> border-collapse text-gray-900 dark:text-gray-100">
@@ -170,14 +174,14 @@ const HelpSearchPage: FC = () => {
<th className="p-2 text-left"> <th className="p-2 text-left">
<SortHeader<HelpOrderField> <SortHeader<HelpOrderField>
by="title" by="title"
label="タイトル" label={label ('shared:labels.title')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
<th className="p-2 text-left"> <th className="p-2 text-left">
<SortHeader<HelpOrderField> <SortHeader<HelpOrderField>
by="updated_at" by="updated_at"
label="有効開始年月日" label={label ('effectiveSince')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
@@ -206,7 +210,7 @@ const HelpSearchPage: FC = () => {
</table> </table>
</div> </div>
{totalPages > 0 && <Pagination page={page} totalPages={totalPages}/>} {totalPages > 0 && <Pagination page={page} totalPages={totalPages}/>}
{results.length === 0 && <p className="mt-4">該当する記事がありません.</p>} {results.length === 0 && <p className="mt-4">{label ('empty')}</p>}
</div>))} </div>))}
</MainArea>) </MainArea>)
} }
+4 -4
ファイルの表示
@@ -8,7 +8,6 @@ import LocalisedText from '@/components/LocalisedText'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import SortHeader from '@/components/SortHeader' import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink' import TagLink from '@/components/TagLink'
import TranslatedText from '@/components/TranslatedText'
import DateTimeField from '@/components/common/DateTimeField' import DateTimeField from '@/components/common/DateTimeField'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
@@ -76,8 +75,7 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
const TagListPage: FC = () => { const TagListPage: FC = () => {
const locale = useLocale () const locale = useLocale ()
const { message } = useUITranslation ('tags') const { message, label } = useUITranslation ('tags')
const label = (key: string) => <TranslatedText namespace="tags" messageKey={key}/>
const behaviourSettings = useClientBehaviourSettings () const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal' const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation () const location = useLocation ()
@@ -265,7 +263,9 @@ const TagListPage: FC = () => {
className={inputClass (invalid)}> className={inputClass (invalid)}>
<option value="">&nbsp;</option> <option value="">&nbsp;</option>
{CATEGORIES.map (cat => ( {CATEGORIES.map (cat => (
<option key={cat} value={cat} <option
key={cat}
value={cat}
lang={message (`common:categories.${ cat }`).language}> lang={message (`common:categories.${ cat }`).language}>
{message (`common:categories.${ cat }`).text} {message (`common:categories.${ cat }`).text}
</option>))} </option>))}
+11 -8
ファイルの表示
@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet-async'
import { useLocation, useParams } from 'react-router-dom' import { useLocation, useParams } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale' import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import PostList from '@/components/PostList' import PostList from '@/components/PostList'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink' import TagLink from '@/components/TagLink'
@@ -25,6 +26,7 @@ import type { Tag } from '@/types'
const WikiDetailPage: FC = () => { const WikiDetailPage: FC = () => {
const { message, label } = useUITranslation ('shared')
const locale = useLocale () const locale = useLocale ()
const params = useParams () const params = useParams ()
const title = params.title ?? '' const title = params.title ?? ''
@@ -128,7 +130,8 @@ const WikiDetailPage: FC = () => {
return ( return (
<MainArea> <MainArea>
<Helmet> <Helmet>
<title>{`${ effectiveTitle }${ deprecated ? '(廃止)' : '' } Wiki | ${ SITE_TITLE }`}</title> <title>{`${ effectiveTitle }${ deprecated
? message ('labels.deprecatedSuffix').text : '' } Wiki | ${ SITE_TITLE }`}</title>
{!(wikiPage?.body) && <meta name="robots" content="noindex"/>} {!(wikiPage?.body) && <meta name="robots" content="noindex"/>}
</Helmet> </Helmet>
@@ -137,15 +140,15 @@ const WikiDetailPage: FC = () => {
border border-gray-700 rounded px-2 py-1 mb-4"> border border-gray-700 rounded px-2 py-1 mb-4">
{wikiPage.pred ? ( {wikiPage.pred ? (
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.pred }`}> <PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.pred }`}>
&lt; 古 {label ('actions.old')}
</PrefetchLink>) : '(最古)'} </PrefetchLink>) : label ('actions.oldest')}
<span>{wikiPage.updatedAt}</span> <span>{wikiPage.updatedAt}</span>
{wikiPage.succ ? ( {wikiPage.succ ? (
<PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.succ }`}> <PrefetchLink to={`/wiki/${ encodeURIComponent (title) }?version=${ wikiPage.succ }`}>
新 &gt; {label ('actions.new')}
</PrefetchLink>) : '(最新)'} </PrefetchLink>) : label ('actions.latest')}
</div>)} </div>)}
<article className="prose dark:prose-invert mx-auto p-4"> <article className="prose dark:prose-invert mx-auto p-4">
@@ -156,14 +159,14 @@ const WikiDetailPage: FC = () => {
withWiki={false} withWiki={false}
withCount={false} withCount={false}
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/> {...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
{deprecated && <span>(廃止)</span>} {deprecated && label ('labels.deprecatedSuffix')}
</h1> </h1>
{waiting ? <div>Loading...</div> : <WikiBody title={title} body={wikiPage?.body}/>} {waiting ? label ('common:loading') : <WikiBody title={title} body={wikiPage?.body}/>}
{(!(version) && posts.length > 0) && ( {(!(version) && posts.length > 0) && (
<div className="not-prose"> <div className="not-prose">
<TabGroup> <TabGroup>
<Tab name="広場"> <Tab name={message ('navigation:posts').text}>
<PostList posts={posts}/> <PostList posts={posts}/>
</Tab> </Tab>
</TabGroup> </TabGroup>
+6 -3
ファイルの表示
@@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet-async'
import { useLocation, useParams } from 'react-router-dom' import { useLocation, useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
@@ -15,6 +16,7 @@ import type { WikiPageDiff } from '@/types'
const WikiDiffPage: FC = () => { const WikiDiffPage: FC = () => {
const { message, label } = useUITranslation ('wiki')
const locale = useLocale () const locale = useLocale ()
const { id } = useParams () const { id } = useParams ()
@@ -26,7 +28,8 @@ const WikiDiffPage: FC = () => {
const from = query.get ('from') const from = query.get ('from')
const to = query.get ('to') const to = query.get ('to')
const displayTitle = diff const displayTitle = diff
? `${ diff.title }${ diff.deprecatedAt != null ? '(廃止)' : '' }` ? `${ diff.title }${ diff.deprecatedAt != null
? message ('shared:labels.deprecatedSuffix').text : '' }`
: '' : ''
useEffect (() => { useEffect (() => {
@@ -38,7 +41,7 @@ const WikiDiffPage: FC = () => {
return ( return (
<MainArea> <MainArea>
<Helmet> <Helmet>
<title>{`Wiki 差分: ${ displayTitle } | ${ SITE_TITLE }`}</title> <title>{message ('diffTitle', { title: displayTitle }).text} | {SITE_TITLE}</title>
</Helmet> </Helmet>
<PageTitle>{displayTitle}</PageTitle> <PageTitle>{displayTitle}</PageTitle>
<div className="prose mx-auto p-4"> <div className="prose mx-auto p-4">
@@ -49,7 +52,7 @@ const WikiDiffPage: FC = () => {
d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}> d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}>
{d.content} {d.content}
</p>))) </p>)))
: 'Loading...'} : label ('common:loading')}
</div> </div>
</MainArea>) </MainArea>)
} }
+11 -8
ファイルの表示
@@ -6,6 +6,7 @@ import MdEditor from 'react-markdown-editor-lite'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale' import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import FieldError from '@/components/common/FieldError' import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
@@ -37,6 +38,7 @@ type WikiFormField = 'title' | 'body'
const WikiEditPage: FC<Props> = ({ user }) => { const WikiEditPage: FC<Props> = ({ user }) => {
const { message, label } = useUITranslation ('wiki')
const locale = useLocale () const locale = useLocale ()
const editable = canEditContent (user) const editable = canEditContent (user)
@@ -66,13 +68,14 @@ const WikiEditPage: FC<Props> = ({ user }) => {
qc.setQueryData (wikiKeys.show (title, { }, locale), qc.setQueryData (wikiKeys.show (title, { }, locale),
(prev: WikiPage) => ({ ...prev, title, body })) (prev: WikiPage) => ({ ...prev, title, body }))
qc.invalidateQueries ({ queryKey: wikiKeys.root }) qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: '投稿成功!' }) toast ({ title: label ('shared:messages.postSuccess') })
navigate (`/wiki/${ title }`) navigate (`/wiki/${ title }`)
} }
catch (e) catch (e)
{ {
applyValidationError (e) applyValidationError (e)
toast ({ title: '投稿失敗', description: '入力を確認してください。' }) toast ({ title: label ('shared:messages.postFailed'),
description: label ('shared:messages.checkInputModern') })
} }
} }
@@ -95,18 +98,18 @@ const WikiEditPage: FC<Props> = ({ user }) => {
return ( return (
<MainArea> <MainArea>
<Helmet> <Helmet>
<title>{`Wiki ページを編輯 | ${ SITE_TITLE }`}</title> <title>{`${ message ('editPage').text } | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
<div className="max-w-xl mx-auto p-4 space-y-4"> <div className="max-w-xl mx-auto p-4 space-y-4">
<h1 className="text-2xl font-bold mb-2">Wiki ページを編輯</h1> <h1 className="text-2xl font-bold mb-2">{label ('editPage')}</h1>
{loading ? 'Loading...' : ( {loading ? label ('common:loading') : (
<> <>
<FieldError messages={baseErrors}/> <FieldError messages={baseErrors}/>
{/* タイトル */} {/* タイトル */}
{/* TODO: タグ補完 */} {/* TODO: タグ補完 */}
<FormField label="タイトル" messages={fieldErrors.title}> <FormField label={label ('shared:labels.title')} messages={fieldErrors.title}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<input type="text" <input type="text"
value={title} value={title}
@@ -117,7 +120,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
</FormField> </FormField>
{/* 本文 */} {/* 本文 */}
<FormField label="本文" messages={fieldErrors.body}> <FormField label={label ('shared:labels.body')} messages={fieldErrors.body}>
{() => ( {() => (
<MdEditor value={body} <MdEditor value={body}
style={{ height: '500px' }} style={{ height: '500px' }}
@@ -129,7 +132,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
{/* 送信 */} {/* 送信 */}
<button onClick={handleSubmit} <button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
編輯 {label ('shared:actions.edit')}
</button> </button>
</>)} </>)}
</div> </div>
+10 -8
ファイルの表示
@@ -5,6 +5,7 @@ import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
@@ -16,6 +17,7 @@ import type { WikiPageChange } from '@/types'
const WikiHistoryPage: FC = () => { const WikiHistoryPage: FC = () => {
const { message, label } = useUITranslation ('wiki')
const locale = useLocale () const locale = useLocale ()
const [changes, setChanges] = useState<WikiPageChange[]> ([]) const [changes, setChanges] = useState<WikiPageChange[]> ([])
@@ -32,18 +34,18 @@ const WikiHistoryPage: FC = () => {
return ( return (
<MainArea> <MainArea>
<Helmet> <Helmet>
<title>{`Wiki 変更履歴 | ${ SITE_TITLE }`}</title> <title>{message ('history').text} | {SITE_TITLE}</title>
</Helmet> </Helmet>
<PageTitle>Wiki 履歴</PageTitle> <PageTitle>{label ('historyHeading')}</PageTitle>
<table className="table-auto w-full border-collapse"> <table className="table-auto w-full border-collapse">
<thead className="border-b-2 border-black dark:border-white"> <thead className="border-b-2 border-black dark:border-white">
<tr> <tr>
<th></th> <th></th>
<th className="p-2 text-left">タイトル</th> <th className="p-2 text-left">{label ('shared:labels.title')}</th>
<th className="p-2 text-left">変更</th> <th className="p-2 text-left">{label ('change')}</th>
<th className="p-2 text-left">日時</th> <th className="p-2 text-left">{label ('dateTime')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -53,7 +55,7 @@ const WikiHistoryPage: FC = () => {
{change.pred != null && ( {change.pred != null && (
<PrefetchLink <PrefetchLink
to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}> to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
差分 {label ('shared:actions.diff')}
</PrefetchLink>)} </PrefetchLink>)}
</td> </td>
<td className="p-2"> <td className="p-2">
@@ -61,10 +63,10 @@ const WikiHistoryPage: FC = () => {
to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}> to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}>
{change.wikiPage.title} {change.wikiPage.title}
</PrefetchLink> </PrefetchLink>
{change.wikiPage.deprecatedAt != null && <span>(廃止)</span>} {change.wikiPage.deprecatedAt != null && label ('shared:labels.deprecatedSuffix')}
</td> </td>
<td className="p-2"> <td className="p-2">
{change.pred == null ? '新規' : '更新'} {change.pred == null ? label ('navigation:create') : label ('shared:actions.update')}
</td> </td>
<td className="p-2"> <td className="p-2">
<PrefetchLink to={`/users/${ change.user.id }`}> <PrefetchLink to={`/users/${ change.user.id }`}>
+10 -7
ファイルの表示
@@ -7,6 +7,7 @@ import { useLocation } from 'react-router-dom'
import 'react-markdown-editor-lite/lib/index.css' import 'react-markdown-editor-lite/lib/index.css'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale' import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import FieldError from '@/components/common/FieldError' import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
@@ -36,6 +37,7 @@ type WikiFormField = 'title' | 'body'
const WikiNewPage: FC<Props> = ({ user }) => { const WikiNewPage: FC<Props> = ({ user }) => {
const { message, label } = useUITranslation ('wiki')
const locale = useLocale () const locale = useLocale ()
const editable = canEditContent (user) const editable = canEditContent (user)
@@ -67,13 +69,14 @@ const WikiNewPage: FC<Props> = ({ user }) => {
await Promise.all ([ await Promise.all ([
qc.invalidateQueries ({ queryKey: wikiKeys.root }), qc.invalidateQueries ({ queryKey: wikiKeys.root }),
qc.invalidateQueries ({ queryKey: tagsKeys.root })]) qc.invalidateQueries ({ queryKey: tagsKeys.root })])
toast ({ title: '投稿成功!' }) toast ({ title: label ('shared:messages.postSuccess') })
navigate (`/wiki/${ encodeURIComponent (data.title) }`) navigate (`/wiki/${ encodeURIComponent (data.title) }`)
} }
catch (e) catch (e)
{ {
applyValidationError (e) applyValidationError (e)
toast ({ title: '投稿失敗', description: '入力を確認してください。' }) toast ({ title: label ('shared:messages.postFailed'),
description: label ('shared:messages.checkInputModern') })
} }
} }
@@ -83,15 +86,15 @@ const WikiNewPage: FC<Props> = ({ user }) => {
return ( return (
<MainArea> <MainArea>
<Helmet> <Helmet>
<title>{`新規 Wiki ページ | ${ SITE_TITLE }`}</title> <title>{`${ message ('newPage').text } | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
<div className="max-w-xl mx-auto p-4 space-y-4"> <div className="max-w-xl mx-auto p-4 space-y-4">
<h1 className="text-2xl font-bold mb-2">新規 Wiki ページ</h1> <h1 className="text-2xl font-bold mb-2">{label ('newPage')}</h1>
<FieldError messages={baseErrors}/> <FieldError messages={baseErrors}/>
{/* タイトル */} {/* タイトル */}
{/* TODO: タグ補完 */} {/* TODO: タグ補完 */}
<FormField label="タイトル" messages={fieldErrors.title}> <FormField label={label ('shared:labels.title')} messages={fieldErrors.title}>
{({ describedBy, invalid }) => ( {({ describedBy, invalid }) => (
<input type="text" <input type="text"
value={title} value={title}
@@ -102,7 +105,7 @@ const WikiNewPage: FC<Props> = ({ user }) => {
</FormField> </FormField>
{/* 本文 */} {/* 本文 */}
<FormField label="本文" messages={fieldErrors.body}> <FormField label={label ('shared:labels.body')} messages={fieldErrors.body}>
{() => ( {() => (
<MdEditor value={body} <MdEditor value={body}
style={{ height: '500px' }} style={{ height: '500px' }}
@@ -114,7 +117,7 @@ const WikiNewPage: FC<Props> = ({ user }) => {
{/* 送信 */} {/* 送信 */}
<button onClick={handleSubmit} <button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
追加 {label ('shared:actions.add')}
</button> </button>
</div> </div>
</MainArea>) </MainArea>)
+8 -6
ファイルの表示
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale' import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import FormField from '@/components/common/FormField' import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle' import PageTitle from '@/components/common/PageTitle'
@@ -16,6 +17,7 @@ import type { WikiPage } from '@/types'
const WikiSearchPage: FC = () => { const WikiSearchPage: FC = () => {
const { label } = useUITranslation ('shared')
const locale = useLocale () const locale = useLocale ()
const [title, setTitle] = useState ('') const [title, setTitle] = useState ('')
const [text, setText] = useState ('') const [text, setText] = useState ('')
@@ -46,7 +48,7 @@ const WikiSearchPage: FC = () => {
<PageTitle>Wiki</PageTitle> <PageTitle>Wiki</PageTitle>
<form onSubmit={handleSearch} className="space-y-2"> <form onSubmit={handleSearch} className="space-y-2">
{/* タイトル */} {/* タイトル */}
<FormField label="タイトル"> <FormField label={label ('labels.title')}>
{({ invalid }) => ( {({ invalid }) => (
<input type="text" <input type="text"
value={title} value={title}
@@ -55,7 +57,7 @@ const WikiSearchPage: FC = () => {
</FormField> </FormField>
{/* 内容 */} {/* 内容 */}
<FormField label="内容"> <FormField label={label ('labels.content')}>
{({ invalid }) => ( {({ invalid }) => (
<input type="text" <input type="text"
value={text} value={text}
@@ -67,7 +69,7 @@ const WikiSearchPage: FC = () => {
<div className="py-3"> <div className="py-3">
<button type="submit" <button type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"> className="bg-blue-500 text-white px-4 py-2 rounded">
検索 {label ('common:search')}
</button> </button>
</div> </div>
</form> </form>
@@ -77,8 +79,8 @@ const WikiSearchPage: FC = () => {
<table className="table-auto w-full border-collapse"> <table className="table-auto w-full border-collapse">
<thead className="border-b-2 border-black dark:border-white"> <thead className="border-b-2 border-black dark:border-white">
<tr> <tr>
<th className="p-2 text-left">タイトル</th> <th className="p-2 text-left">{label ('labels.title')}</th>
<th className="p-2 text-left">最終更新</th> <th className="p-2 text-left">{label ('labels.lastUpdated')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -89,7 +91,7 @@ const WikiSearchPage: FC = () => {
<PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}> <PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}>
{page.title} {page.title}
</PrefetchLink> </PrefetchLink>
{page.deprecatedAt != null && <span>(廃止)</span>} {page.deprecatedAt != null && label ('labels.deprecatedSuffix')}
</td> </td>
<td className="p-2"> <td className="p-2">
{dateString (page.updatedAt)} {dateString (page.updatedAt)}