このコミットが含まれているのは:
2026-10-18 00:07:07 +09:00
コミット 63860c94af
12個のファイルの変更、280行の追加、66行の削除
+3 -1
ファイルの表示
@@ -2,10 +2,12 @@ import { useLocation } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import type { ReactNode } from 'react'
export default <T extends string,>({ by, label, currentOrder, defaultDirection }: { export default <T extends string,>({ by, label, currentOrder, defaultDirection }: {
by: T by: T
label: string label: ReactNode
currentOrder: `${ T }:${ 'asc' | 'desc' }` currentOrder: `${ T }:${ 'asc' | 'desc' }`
defaultDirection: Record<T, 'asc' | 'desc'> }) => { defaultDirection: Record<T, 'asc' | 'desc'> }) => {
const [fld, dir] = currentOrder.split (':') const [fld, dir] = currentOrder.split (':')
+11 -3
ファイルの表示
@@ -1,5 +1,6 @@
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import ResponsiveMarqueeText from '@/components/ResponsiveMarqueeText' import ResponsiveMarqueeText from '@/components/ResponsiveMarqueeText'
import { useUITranslation } from '@/lib/useUITranslation'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { ComponentProps, CSSProperties, FC, HTMLAttributes } from 'react' import type { ComponentProps, CSSProperties, FC, HTMLAttributes } from 'react'
@@ -67,6 +68,10 @@ const TagLink: FC<Props> = ({ tag,
style, style,
title, title,
...props }) => { ...props }) => {
const { message } = useUITranslation ()
const missingMaterial = message ('tagLink.missingMaterial', { name: tag.name })
const missingDeerjikist = message ('tagLink.missingDeerjikist', { name: tag.name })
const missingWiki = message ('tagLink.missingWiki', { name: tag.name })
const colourStyle = { const colourStyle = {
'--tag-link-colour': `var(--tag-colour-${ tag.category })`, '--tag-link-colour': `var(--tag-colour-${ tag.category })`,
'--tag-link-hover-colour': `var(--tag-colour-${ tag.category }-hover)`, '--tag-link-hover-colour': `var(--tag-colour-${ tag.category }-hover)`,
@@ -121,7 +126,8 @@ const TagLink: FC<Props> = ({ tag,
to={`/materials/new?tag=${ encodeURIComponent (tag.name) }`} to={`/materials/new?tag=${ encodeURIComponent (tag.name) }`}
className="animate-[wiki-blink_.25s_steps(2,end)_infinite] className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]" dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
title={`${ tag.name } 素材情報が存在しません.`}> lang={missingMaterial.language}
title={missingMaterial.text}>
! !
</PrefetchLink>) </PrefetchLink>)
: ( : (
@@ -131,7 +137,8 @@ const TagLink: FC<Props> = ({ tag,
to={`/tags/${ tag.id }/deerjikists`} to={`/tags/${ tag.id }/deerjikists`}
className="animate-[wiki-blink_.25s_steps(2,end)_infinite] className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]" dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
title={`${ tag.name } に関する情報が存在しません.`}> lang={missingDeerjikist.language}
title={missingDeerjikist.text}>
! !
</PrefetchLink>) </PrefetchLink>)
: ( : (
@@ -139,7 +146,8 @@ const TagLink: FC<Props> = ({ tag,
to={`/wiki/${ encodeURIComponent (tag.name) }`} to={`/wiki/${ encodeURIComponent (tag.name) }`}
className="animate-[wiki-blink_.25s_steps(2,end)_infinite] className="animate-[wiki-blink_.25s_steps(2,end)_infinite]
dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]" dark:animate-[wiki-blink-dark_.25s_steps(2,end)_infinite]"
title={`${ tag.name } Wiki が存在しません.`}> lang={missingWiki.language}
title={missingWiki.text}>
! !
</PrefetchLink>)))} </PrefetchLink>)))}
</span>)} </span>)}
+5 -8
ファイルの表示
@@ -1,7 +1,5 @@
import { useTranslation } from 'react-i18next'
import LocalisedText from '@/components/LocalisedText' import LocalisedText from '@/components/LocalisedText'
import { useLocale } from '@/lib/useLocale' import { useUITranslation } from '@/lib/useUITranslation'
type Props = { type Props = {
messageKey: string messageKey: string
@@ -13,12 +11,11 @@ type Props = {
const TranslatedText = ( const TranslatedText = (
{ messageKey, namespace = 'navigation', values, className }: Props, { messageKey, namespace = 'navigation', values, className }: Props,
) => { ) => {
const locale = useLocale () const { message } = useUITranslation (namespace)
const { t } = useTranslation (namespace, { lng: locale }) const result = message (messageKey, values)
const result = t (messageKey, { ...values, returnDetails: true })
return ( return (
<LocalisedText language={result.usedLng} className={className}> <LocalisedText language={result.language} className={className}>
{result.res} {result.text}
</LocalisedText>) </LocalisedText>)
} }
+13 -5
ファイルの表示
@@ -1,6 +1,7 @@
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 type { FC } from 'react' import type { FC } from 'react'
@@ -49,6 +50,7 @@ const getPages = (
const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => { const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
const location = useLocation () const location = useLocation ()
const { message } = useUITranslation ()
const buildTo = (p: number) => { const buildTo = (p: number) => {
const qs = new URLSearchParams (location.search) const qs = new URLSearchParams (location.search)
@@ -59,7 +61,9 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
const pages = getPages (page, totalPages, siblingCount) const pages = getPages (page, totalPages, siblingCount)
return ( return (
<nav className="mt-4 flex justify-center" aria-label="Pagination"> <nav className="mt-4 flex justify-center"
lang={message ('pagination.label').language}
aria-label={message ('pagination.label').text}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{(page > 1) {(page > 1)
? ( ? (
@@ -67,13 +71,15 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
<PrefetchLink <PrefetchLink
className="md:hidden p-2" className="md:hidden p-2"
to={buildTo (1)} to={buildTo (1)}
aria-label="最初のページ"> lang={message ('pagination.first').language}
aria-label={message ('pagination.first').text}>
|&lt; |&lt;
</PrefetchLink> </PrefetchLink>
<PrefetchLink <PrefetchLink
className="p-2" className="p-2"
to={buildTo (page - 1)} to={buildTo (page - 1)}
aria-label="前のページ"> lang={message ('pagination.previous').language}
aria-label={message ('pagination.previous').text}>
&lt; &lt;
</PrefetchLink> </PrefetchLink>
</>) </>)
@@ -106,13 +112,15 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
<PrefetchLink <PrefetchLink
className="p-2" className="p-2"
to={buildTo (page + 1)} to={buildTo (page + 1)}
aria-label="次のページ"> lang={message ('pagination.next').language}
aria-label={message ('pagination.next').text}>
&gt; &gt;
</PrefetchLink> </PrefetchLink>
<PrefetchLink <PrefetchLink
className="md:hidden p-2" className="md:hidden p-2"
to={buildTo (totalPages)} to={buildTo (totalPages)}
aria-label="最後のページ"> lang={message ('pagination.last').language}
aria-label={message ('pagination.last').text}>
&gt;| &gt;|
</PrefetchLink> </PrefetchLink>
</>) </>)
+36 -6
ファイルの表示
@@ -2,12 +2,36 @@ import { createInstance } from 'i18next'
import { initReactI18next } from 'react-i18next' import { initReactI18next } from 'react-i18next'
import ainuNavigation from '@/locales/ain-Kana/navigation.json' 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 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 ()
// Keep Japanese placeholders editable without reporting them as completed translations.
const translatedResource = (resource: Record<string, unknown>): Record<string, unknown> => {
const untranslated = new Set (
Array.isArray (resource._untranslated) ? resource._untranslated : [])
const select = (entries: Record<string, unknown>, prefix = ''): Record<string, unknown> =>
Object.fromEntries (Object.entries (entries).flatMap (([key, value]) => {
const path = prefix ? `${ prefix }.${ key }` : key
if (key === '_untranslated' || untranslated.has (path))
return []
const translated =
(value != null && typeof value === 'object' && !(Array.isArray (value)))
? select (value as Record<string, unknown>, path)
: value
return [[key, translated]]
}))
return select (resource)
}
void i18n.use (initReactI18next).init ({ void i18n.use (initReactI18next).init ({
lng: 'ja', lng: 'ja',
fallbackLng: 'ja', fallbackLng: 'ja',
@@ -15,10 +39,13 @@ void i18n.use (initReactI18next).init ({
load: 'currentOnly', load: 'currentOnly',
initAsync: false, initAsync: false,
defaultNS: 'navigation', defaultNS: 'navigation',
ns: ['navigation'], ns: ['navigation', 'common', 'tags'],
resources: { resources: {
ja: { navigation: nipponeseNavigation }, ja: { navigation: nipponeseNavigation, common: nipponeseCommon, tags: nipponeseTags },
'ain-Kana': { navigation: ainuNavigation } }, 'ain-Kana': {
navigation: ainuNavigation,
common: translatedResource (ainuCommon),
tags: translatedResource (ainuTags) } },
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
returnNull: false, returnNull: false,
returnEmptyString: false, returnEmptyString: false,
@@ -50,7 +77,10 @@ export const untranslatedKeys = (
if (import.meta.env.DEV) if (import.meta.env.DEV)
{ {
const missing = untranslatedKeys ('ain-Kana') for (const namespace of ['navigation', 'common', 'tags'])
if (missing.length > 0) {
console.warn ('[i18n] ain-Kana: untranslated navigation keys', missing) const missing = untranslatedKeys ('ain-Kana', namespace)
if (missing.length > 0)
console.warn (`[i18n] ain-Kana: untranslated ${ namespace } keys`, missing)
}
} }
+16
ファイルの表示
@@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next'
import { useLocale } from '@/lib/useLocale'
export const useUITranslation = (namespace = 'common') => {
const locale = useLocale ()
const { t } = useTranslation (namespace, { lng: locale })
const message = (key: string, values?: Record<string, string | number>) => {
const result = t (key, { ...values, returnDetails: true })
return { text: result.res, language: result.usedLng }
}
return { message }
}
+20
ファイルの表示
@@ -3,6 +3,8 @@ import { twMerge } from 'tailwind-merge'
import type { ClassValue } from 'clsx' import type { ClassValue } from 'clsx'
import type { LocaleCode } from '@/lib/locales'
export const toDate = (d: string | Date): Date => typeof d === 'string' ? new Date (d) : d export const toDate = (d: string | Date): Date => typeof d === 'string' ? new Date (d) : d
@@ -25,6 +27,24 @@ export const dateString = (
second: unknown == null ? 'numeric' : undefined }) second: unknown == null ? 'numeric' : undefined })
export const localisedDateString = (value: string | Date, locale: LocaleCode): string => {
if (locale === 'ja')
return dateString (value)
const date = toDate (value)
if (Number.isNaN (date.getTime ()))
return dateString (value)
const parts = new Intl.DateTimeFormat ('en-CA', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hourCycle: 'h23' }).formatToParts (date)
const part = (type: string) => parts.find (entry => entry.type === type)?.value ?? ''
return `${ part ('year') }-${ part ('month') }-${ part ('day') }\u3000${
part ('hour') }:${ part ('minute') }:${ part ('second') }`
}
export const originalCreatedAtString = ( export const originalCreatedAtString = (
f: string | Date | null, f: string | Date | null,
b: string | Date | null): string => { b: string | Date | null): string => {
+38
ファイルの表示
@@ -0,0 +1,38 @@
{
"_untranslated": [
"countLabel", "categoryLabel", "stateLabel", "loading", "fetchError",
"states", "categories.deerjikist", "categories.meme", "categories.character",
"categories.general", "categories.meta", "categories.nico", "pagination", "tagLink"
],
"countLabel": "件数",
"categoryLabel": "カテゴリ",
"stateLabel": "状態",
"search": "イフナラ",
"loading": "Loading...",
"fetchError": "データ取得エラー",
"states": {
"active": "有効",
"deprecated": "廃止"
},
"categories": {
"deerjikist": "ニジラー",
"meme": "原作・ネタ元・ミーム等",
"character": "キャラクター",
"general": "一般",
"material": "チカㇻペ",
"meta": "メタタグ",
"nico": "ニコニコタグ"
},
"pagination": {
"label": "Pagination",
"first": "最初のページ",
"previous": "前のページ",
"next": "次のページ",
"last": "最後のページ"
},
"tagLink": {
"missingMaterial": "{{name}} 素材情報が存在しません.",
"missingDeerjikist": "{{name}} に関する情報が存在しません.",
"missingWiki": "{{name}} Wiki が存在しません."
}
}
+17
ファイルの表示
@@ -0,0 +1,17 @@
{
"_untranslated": [
"name", "postCount", "firstRecordedAt", "definitionUpdatedAt", "aliases",
"parents", "firstRecordedColumn", "updatedAt", "empty"
],
"title": "タㇰ",
"name": "名前",
"postCount": "広場の投稿数",
"firstRecordedAt": "はじめて記載された日時",
"definitionUpdatedAt": "定義の更新日時",
"aliases": "別名",
"parents": "上位タグ",
"firstRecordedColumn": "最初の記載日時",
"updatedAt": "更新日時",
"history": "フㇱコアンペ",
"empty": "結果ないよ(笑)"
}
+33
ファイルの表示
@@ -0,0 +1,33 @@
{
"countLabel": "件数",
"categoryLabel": "カテゴリ",
"stateLabel": "状態",
"search": "検索",
"loading": "Loading...",
"fetchError": "データ取得エラー",
"states": {
"active": "有効",
"deprecated": "廃止"
},
"categories": {
"deerjikist": "ニジラー",
"meme": "原作・ネタ元・ミーム等",
"character": "キャラクター",
"general": "一般",
"material": "素材",
"meta": "メタタグ",
"nico": "ニコニコタグ"
},
"pagination": {
"label": "Pagination",
"first": "最初のページ",
"previous": "前のページ",
"next": "次のページ",
"last": "最後のページ"
},
"tagLink": {
"missingMaterial": "{{name}} 素材情報が存在しません.",
"missingDeerjikist": "{{name}} に関する情報が存在しません.",
"missingWiki": "{{name}} Wiki が存在しません."
}
}
+13
ファイルの表示
@@ -0,0 +1,13 @@
{
"title": "タグ",
"name": "名前",
"postCount": "広場の投稿数",
"firstRecordedAt": "はじめて記載された日時",
"definitionUpdatedAt": "定義の更新日時",
"aliases": "別名",
"parents": "上位タグ",
"firstRecordedColumn": "最初の記載日時",
"updatedAt": "更新日時",
"history": "耕作履歴",
"empty": "結果ないよ(笑)"
}
+75 -43
ファイルの表示
@@ -4,16 +4,18 @@ import { Helmet } from 'react-helmet-async'
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 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'
import Pagination from '@/components/common/Pagination' import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config' import { SITE_TITLE } from '@/config'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts' import { CATEGORIES } from '@/consts'
import { clientScrollBehaviour } from '@/lib/clientAnimation' import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { tagsKeys } from '@/lib/queryKeys' import { tagsKeys } from '@/lib/queryKeys'
import { import {
@@ -26,8 +28,9 @@ import {
} from '@/lib/settings' } from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts' import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { useUITranslation } from '@/lib/useUITranslation'
import { fetchTags } from '@/lib/tags' import { fetchTags } from '@/lib/tags'
import { dateString, inputClass } from '@/lib/utils' import { inputClass, localisedDateString } from '@/lib/utils'
import type { FC, FormEvent } from 'react' import type { FC, FormEvent } from 'react'
@@ -45,8 +48,6 @@ const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ()) ['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
const parseLimit = (value: string | null): 20 | 50 | 100 | null => { const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
const n = Number (value) const n = Number (value)
return n === 20 || n === 50 || n === 100 ? n : null return n === 20 || n === 50 || n === 100 ? n : null
@@ -75,6 +76,8 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
const TagListPage: FC = () => { const TagListPage: FC = () => {
const locale = useLocale () const locale = useLocale ()
const { message } = 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 ()
@@ -99,7 +102,8 @@ const TagListPage: FC = () => {
const qCreatedTo = query.get ('created_to') ?? '' const qCreatedTo = query.get ('created_to') ?? ''
const qUpdatedFrom = query.get ('updated_from') ?? '' const qUpdatedFrom = query.get ('updated_from') ?? ''
const qUpdatedTo = query.get ('updated_to') ?? '' const qUpdatedTo = query.get ('updated_to') ?? ''
const qDeprecated = query.has ('deprecated') const qDeprecated =
query.has ('deprecated')
? boolFromQuery (query.get ('deprecated')) ? boolFromQuery (query.get ('deprecated'))
: null : null
const order = ( const order = (
@@ -130,7 +134,7 @@ const TagListPage: FC = () => {
updatedFrom: qUpdatedFrom, updatedFrom: qUpdatedFrom,
updatedTo: qUpdatedTo, updatedTo: qUpdatedTo,
deprecated: qDeprecated } deprecated: qDeprecated }
const { data, isLoading: loading } = useQuery ({ const { data, isLoading: loading, isError } = useQuery ({
queryKey: tagsKeys.index (keys, locale), queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) }) queryFn: () => fetchTags (keys, locale) })
const results = data?.tags ?? [] const results = data?.tags ?? []
@@ -203,32 +207,28 @@ const TagListPage: FC = () => {
useKeyboardShortcuts (useMemo (() => ({ useKeyboardShortcuts (useMemo (() => ({
...(page > 1 ...(page > 1
? { ? { 'pagination.previous': () => {
'pagination.previous': () => {
updatePage (page - 1) updatePage (page - 1)
}, } }
}
: { }), : { }),
...(totalPages > 0 && page < totalPages ...(totalPages > 0 && page < totalPages
? { ? { 'pagination.next': () => {
'pagination.next': () => {
updatePage (Math.min (page + 1, totalPages)) updatePage (Math.min (page + 1, totalPages))
}, } }
}
: { }), : { }),
}), [page, totalPages, location.search])) }), [page, totalPages, location.search]))
return ( return (
<MainArea> <MainArea>
<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 ('title')}</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 <select
value={limit} value={limit}
onChange={ev => updateListQuery ({ onChange={ev => updateListQuery ({
@@ -246,7 +246,7 @@ const TagListPage: FC = () => {
<form onSubmit={handleSearch} className="space-y-2"> <form onSubmit={handleSearch} className="space-y-2">
{/* 名前 */} {/* 名前 */}
<FormField label="名前"> <FormField label={label ('name')}>
{({ invalid }) => ( {({ invalid }) => (
<input <input
type="text" type="text"
@@ -257,7 +257,7 @@ const TagListPage: FC = () => {
</FormField> </FormField>
{/* カテゴリ */} {/* カテゴリ */}
<FormField label="カテゴリ"> <FormField label={label ('common:categoryLabel')}>
{({ invalid }) => ( {({ invalid }) => (
<select <select
value={category ?? ''} value={category ?? ''}
@@ -265,14 +265,15 @@ 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}
{CATEGORY_NAMES[cat]} lang={message (`common:categories.${ cat }`).language}>
{message (`common:categories.${ cat }`).text}
</option>))} </option>))}
</select>)} </select>)}
</FormField> </FormField>
{/* 広場の投稿数 */} {/* 広場の投稿数 */}
<FormField label="広場の投稿数"> <FormField label={label ('postCount')}>
{({ invalid }) => ( {({ invalid }) => (
<> <>
<input <input
@@ -294,34 +295,38 @@ const TagListPage: FC = () => {
</FormField> </FormField>
{/* はじめて記載された日時 */} {/* はじめて記載された日時 */}
<FormField label="はじめて記載された日時"> <FormField label={label ('firstRecordedAt')}>
{() => ( {() => (
<> <>
<DateTimeField <DateTimeField
lang={locale}
value={createdFrom ?? undefined} value={createdFrom ?? undefined}
onChange={setCreatedFrom}/> onChange={setCreatedFrom}/>
<span className="mx-1">〜</span> <span className="mx-1">〜</span>
<DateTimeField <DateTimeField
lang={locale}
value={createdTo ?? undefined} value={createdTo ?? undefined}
onChange={setCreatedTo}/> onChange={setCreatedTo}/>
</>)} </>)}
</FormField> </FormField>
{/* 定義の更新日時 */} {/* 定義の更新日時 */}
<FormField label="定義の更新日時"> <FormField label={label ('definitionUpdatedAt')}>
{() => ( {() => (
<> <>
<DateTimeField <DateTimeField
lang={locale}
value={updatedFrom ?? undefined} value={updatedFrom ?? undefined}
onChange={setUpdatedFrom}/> onChange={setUpdatedFrom}/>
<span className="mx-1">〜</span> <span className="mx-1">〜</span>
<DateTimeField <DateTimeField
lang={locale}
value={updatedTo ?? undefined} value={updatedTo ?? undefined}
onChange={setUpdatedTo}/> onChange={setUpdatedTo}/>
</>)} </>)}
</FormField> </FormField>
<FormField label="状態"> <FormField label={label ('common:stateLabel')}>
{({ invalid }) => ( {({ invalid }) => (
<select <select
value={deprecated == null ? '' : (deprecated ? '1' : '0')} value={deprecated == null ? '' : (deprecated ? '1' : '0')}
@@ -331,8 +336,12 @@ const TagListPage: FC = () => {
: e.target.value === '1')} : e.target.value === '1')}
className={inputClass (invalid)}> className={inputClass (invalid)}>
<option value="">&nbsp;</option> <option value="">&nbsp;</option>
<option value="0">有効</option> <option value="0" lang={message ('common:states.active').language}>
<option value="1">廃止</option> {message ('common:states.active').text}
</option>
<option value="1" lang={message ('common:states.deprecated').language}>
{message ('common:states.deprecated').text}
</option>
</select>)} </select>)}
</FormField> </FormField>
@@ -340,13 +349,14 @@ const TagListPage: FC = () => {
<button <button
type="submit" 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...' : (results.length > 0 ? ( {isError && <p role="alert">{label ('common:fetchError')}</p>}
{loading ? label ('common:loading') : (results.length > 0 ? (
<div className="mt-4"> <div className="mt-4">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full min-w-[2000px] table-fixed border-collapse"> <table className="w-full min-w-[2000px] table-fixed border-collapse">
@@ -367,38 +377,44 @@ const TagListPage: FC = () => {
<th className="p-2 text-left whitespace-nowrap"> <th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField> <SortHeader<FetchTagsOrderField>
by="name" by="name"
label="タグ" label={label ('title')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
<th className="p-2 text-left whitespace-nowrap"> <th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField> <SortHeader<FetchTagsOrderField>
by="post_count" by="post_count"
label="件数" label={label ('common:countLabel')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
<th className="p-2 text-left whitespace-nowrap"> <th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField> <SortHeader<FetchTagsOrderField>
by="category" by="category"
label="カテゴリ" label={label ('common:categoryLabel')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
<th className="p-2 text-left whitespace-nowrap">状態</th> <th className="p-2 text-left whitespace-nowrap">
<th className="p-2 text-left whitespace-nowrap">別名</th> {label ('common:stateLabel')}
<th className="p-2 text-left whitespace-nowrap">上位タグ</th> </th>
<th className="p-2 text-left whitespace-nowrap">
{label ('aliases')}
</th>
<th className="p-2 text-left whitespace-nowrap">
{label ('parents')}
</th>
<th className="p-2 text-left whitespace-nowrap"> <th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField> <SortHeader<FetchTagsOrderField>
by="created_at" by="created_at"
label="最初の記載日時" label={label ('firstRecordedColumn')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
<th className="p-2 text-left whitespace-nowrap"> <th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField> <SortHeader<FetchTagsOrderField>
by="updated_at" by="updated_at"
label="更新日時" label={label ('updatedAt')}
currentOrder={order} currentOrder={order}
defaultDirection={defaultDirection}/> defaultDirection={defaultDirection}/>
</th> </th>
@@ -420,8 +436,12 @@ const TagListPage: FC = () => {
withCount={false}/> withCount={false}/>
</td> </td>
<td className="p-2 text-right">{row.postCount}</td> <td className="p-2 text-right">{row.postCount}</td>
<td className="p-2">{CATEGORY_NAMES[row.category]}</td> <td className="p-2">
<td className="p-2">{tagStateLabel (row.deprecatedAt)}</td> {label (`common:categories.${ row.category }`)}
</td>
<td className="p-2">
{row.deprecatedAt ? label ('common:states.deprecated') : ''}
</td>
<td className="p-2">{row.aliases.join (' ')}</td> <td className="p-2">{row.aliases.join (' ')}</td>
<td className="p-2"> <td className="p-2">
{row.parents.map (t => ( {row.parents.map (t => (
@@ -432,14 +452,26 @@ const TagListPage: FC = () => {
withCount={false}/> withCount={false}/>
</span>))} </span>))}
</td> </td>
<td className="p-2">{dateString (row.createdAt)}</td> <td className="p-2">
<td className="p-2">{dateString (row.updatedAt)}</td> <time dateTime={row.createdAt} lang={locale}>
<LocalisedText language={locale}>
{localisedDateString (row.createdAt, locale)}
</LocalisedText>
</time>
</td>
<td className="p-2">
<time dateTime={row.updatedAt} lang={locale}>
<LocalisedText language={locale}>
{localisedDateString (row.updatedAt, locale)}
</LocalisedText>
</time>
</td>
<td className="p-2"> <td className="p-2">
<PrefetchLink <PrefetchLink
to={row.category === 'nico' to={row.category === 'nico'
? `/posts/changes?external_tag=${ row.id }` ? `/posts/changes?external_tag=${ row.id }`
: `/posts/changes?tag=${ row.id }`}> : `/posts/changes?tag=${ row.id }`}>
耕作履歴 {label ('history')}
</PrefetchLink> </PrefetchLink>
</td> </td>
</tr>))} </tr>))}
@@ -448,7 +480,7 @@ const TagListPage: FC = () => {
</div> </div>
<Pagination page={page} totalPages={totalPages}/> <Pagination page={page} totalPages={totalPages}/>
</div>) : '結果ないよ(笑)')} </div>) : (isError ? null : label ('empty')))}
</MainArea>) </MainArea>)
} }