このコミットが含まれているのは:
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 type { ReactNode } from 'react'
export default <T extends string,>({ by, label, currentOrder, defaultDirection }: {
by: T
label: string
label: ReactNode
currentOrder: `${ T }:${ 'asc' | 'desc' }`
defaultDirection: Record<T, 'asc' | 'desc'> }) => {
const [fld, dir] = currentOrder.split (':')
+11 -3
ファイルの表示
@@ -1,5 +1,6 @@
import PrefetchLink from '@/components/PrefetchLink'
import ResponsiveMarqueeText from '@/components/ResponsiveMarqueeText'
import { useUITranslation } from '@/lib/useUITranslation'
import { cn } from '@/lib/utils'
import type { ComponentProps, CSSProperties, FC, HTMLAttributes } from 'react'
@@ -67,6 +68,10 @@ const TagLink: FC<Props> = ({ tag,
style,
title,
...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 = {
'--tag-link-colour': `var(--tag-colour-${ tag.category })`,
'--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) }`}
className="animate-[wiki-blink_.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>)
: (
@@ -131,7 +137,8 @@ const TagLink: FC<Props> = ({ tag,
to={`/tags/${ tag.id }/deerjikists`}
className="animate-[wiki-blink_.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>)
: (
@@ -139,7 +146,8 @@ const TagLink: FC<Props> = ({ tag,
to={`/wiki/${ encodeURIComponent (tag.name) }`}
className="animate-[wiki-blink_.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>)))}
</span>)}
+5 -8
ファイルの表示
@@ -1,7 +1,5 @@
import { useTranslation } from 'react-i18next'
import LocalisedText from '@/components/LocalisedText'
import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
type Props = {
messageKey: string
@@ -13,12 +11,11 @@ type Props = {
const TranslatedText = (
{ messageKey, namespace = 'navigation', values, className }: Props,
) => {
const locale = useLocale ()
const { t } = useTranslation (namespace, { lng: locale })
const result = t (messageKey, { ...values, returnDetails: true })
const { message } = useUITranslation (namespace)
const result = message (messageKey, values)
return (
<LocalisedText language={result.usedLng} className={className}>
{result.res}
<LocalisedText language={result.language} className={className}>
{result.text}
</LocalisedText>)
}
+13 -5
ファイルの表示
@@ -1,6 +1,7 @@
import { useLocation } from 'react-router-dom'
import PrefetchLink from '@/components/PrefetchLink'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react'
@@ -49,6 +50,7 @@ const getPages = (
const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
const location = useLocation ()
const { message } = useUITranslation ()
const buildTo = (p: number) => {
const qs = new URLSearchParams (location.search)
@@ -59,7 +61,9 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
const pages = getPages (page, totalPages, siblingCount)
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">
{(page > 1)
? (
@@ -67,13 +71,15 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
<PrefetchLink
className="md:hidden p-2"
to={buildTo (1)}
aria-label="最初のページ">
lang={message ('pagination.first').language}
aria-label={message ('pagination.first').text}>
|&lt;
</PrefetchLink>
<PrefetchLink
className="p-2"
to={buildTo (page - 1)}
aria-label="前のページ">
lang={message ('pagination.previous').language}
aria-label={message ('pagination.previous').text}>
&lt;
</PrefetchLink>
</>)
@@ -106,13 +112,15 @@ const Pagination: FC<Props> = ({ page, totalPages, siblingCount = 3 }) => {
<PrefetchLink
className="p-2"
to={buildTo (page + 1)}
aria-label="次のページ">
lang={message ('pagination.next').language}
aria-label={message ('pagination.next').text}>
&gt;
</PrefetchLink>
<PrefetchLink
className="md:hidden p-2"
to={buildTo (totalPages)}
aria-label="最後のページ">
lang={message ('pagination.last').language}
aria-label={message ('pagination.last').text}>
&gt;|
</PrefetchLink>
</>)
+36 -6
ファイルの表示
@@ -2,12 +2,36 @@ import { createInstance } from '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'
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 ({
lng: 'ja',
fallbackLng: 'ja',
@@ -15,10 +39,13 @@ void i18n.use (initReactI18next).init ({
load: 'currentOnly',
initAsync: false,
defaultNS: 'navigation',
ns: ['navigation'],
ns: ['navigation', 'common', 'tags'],
resources: {
ja: { navigation: nipponeseNavigation },
'ain-Kana': { navigation: ainuNavigation } },
ja: { navigation: nipponeseNavigation, common: nipponeseCommon, tags: nipponeseTags },
'ain-Kana': {
navigation: ainuNavigation,
common: translatedResource (ainuCommon),
tags: translatedResource (ainuTags) } },
interpolation: { escapeValue: false },
returnNull: false,
returnEmptyString: false,
@@ -50,7 +77,10 @@ export const untranslatedKeys = (
if (import.meta.env.DEV)
{
const missing = untranslatedKeys ('ain-Kana')
if (missing.length > 0)
console.warn ('[i18n] ain-Kana: untranslated navigation keys', missing)
for (const namespace of ['navigation', 'common', 'tags'])
{
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 { LocaleCode } from '@/lib/locales'
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 })
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 = (
f: string | Date | null,
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 { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import LocalisedText from '@/components/LocalisedText'
import PrefetchLink from '@/components/PrefetchLink'
import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink'
import TranslatedText from '@/components/TranslatedText'
import DateTimeField from '@/components/common/DateTimeField'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { CATEGORIES } from '@/consts'
import { clientScrollBehaviour } from '@/lib/clientAnimation'
import { tagsKeys } from '@/lib/queryKeys'
import {
@@ -26,8 +28,9 @@ import {
} from '@/lib/settings'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useKeyboardShortcuts } from '@/lib/useKeyboardShortcuts'
import { useUITranslation } from '@/lib/useUITranslation'
import { fetchTags } from '@/lib/tags'
import { dateString, inputClass } from '@/lib/utils'
import { inputClass, localisedDateString } from '@/lib/utils'
import type { FC, FormEvent } from 'react'
@@ -45,8 +48,6 @@ const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
const n = Number (value)
return n === 20 || n === 50 || n === 100 ? n : null
@@ -75,6 +76,8 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
const TagListPage: FC = () => {
const locale = useLocale ()
const { message } = useUITranslation ('tags')
const label = (key: string) => <TranslatedText namespace="tags" messageKey={key}/>
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
@@ -99,7 +102,8 @@ const TagListPage: FC = () => {
const qCreatedTo = query.get ('created_to') ?? ''
const qUpdatedFrom = query.get ('updated_from') ?? ''
const qUpdatedTo = query.get ('updated_to') ?? ''
const qDeprecated = query.has ('deprecated')
const qDeprecated =
query.has ('deprecated')
? boolFromQuery (query.get ('deprecated'))
: null
const order = (
@@ -130,7 +134,7 @@ const TagListPage: FC = () => {
updatedFrom: qUpdatedFrom,
updatedTo: qUpdatedTo,
deprecated: qDeprecated }
const { data, isLoading: loading } = useQuery ({
const { data, isLoading: loading, isError } = useQuery ({
queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) })
const results = data?.tags ?? []
@@ -203,32 +207,28 @@ const TagListPage: FC = () => {
useKeyboardShortcuts (useMemo (() => ({
...(page > 1
? {
'pagination.previous': () => {
? { 'pagination.previous': () => {
updatePage (page - 1)
},
}
} }
: { }),
...(totalPages > 0 && page < totalPages
? {
'pagination.next': () => {
? { 'pagination.next': () => {
updatePage (Math.min (page + 1, totalPages))
},
}
} }
: { }),
}), [page, totalPages, location.search]))
return (
<MainArea>
<Helmet>
<title>タグ | {SITE_TITLE}</title>
<title>{message ('title').text} | {SITE_TITLE}</title>
</Helmet>
<div className="max-w-xl">
<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">
<span>件数</span>
<span>{label ('common:countLabel')}</span>
<select
value={limit}
onChange={ev => updateListQuery ({
@@ -246,7 +246,7 @@ const TagListPage: FC = () => {
<form onSubmit={handleSearch} className="space-y-2">
{/* 名前 */}
<FormField label="名前">
<FormField label={label ('name')}>
{({ invalid }) => (
<input
type="text"
@@ -257,7 +257,7 @@ const TagListPage: FC = () => {
</FormField>
{/* カテゴリ */}
<FormField label="カテゴリ">
<FormField label={label ('common:categoryLabel')}>
{({ invalid }) => (
<select
value={category ?? ''}
@@ -265,14 +265,15 @@ const TagListPage: FC = () => {
className={inputClass (invalid)}>
<option value="">&nbsp;</option>
{CATEGORIES.map (cat => (
<option key={cat} value={cat}>
{CATEGORY_NAMES[cat]}
<option key={cat} value={cat}
lang={message (`common:categories.${ cat }`).language}>
{message (`common:categories.${ cat }`).text}
</option>))}
</select>)}
</FormField>
{/* 広場の投稿数 */}
<FormField label="広場の投稿数">
<FormField label={label ('postCount')}>
{({ invalid }) => (
<>
<input
@@ -294,34 +295,38 @@ const TagListPage: FC = () => {
</FormField>
{/* はじめて記載された日時 */}
<FormField label="はじめて記載された日時">
<FormField label={label ('firstRecordedAt')}>
{() => (
<>
<DateTimeField
lang={locale}
value={createdFrom ?? undefined}
onChange={setCreatedFrom}/>
<span className="mx-1">〜</span>
<DateTimeField
lang={locale}
value={createdTo ?? undefined}
onChange={setCreatedTo}/>
</>)}
</FormField>
{/* 定義の更新日時 */}
<FormField label="定義の更新日時">
<FormField label={label ('definitionUpdatedAt')}>
{() => (
<>
<DateTimeField
lang={locale}
value={updatedFrom ?? undefined}
onChange={setUpdatedFrom}/>
<span className="mx-1">〜</span>
<DateTimeField
lang={locale}
value={updatedTo ?? undefined}
onChange={setUpdatedTo}/>
</>)}
</FormField>
<FormField label="状態">
<FormField label={label ('common:stateLabel')}>
{({ invalid }) => (
<select
value={deprecated == null ? '' : (deprecated ? '1' : '0')}
@@ -331,8 +336,12 @@ const TagListPage: FC = () => {
: e.target.value === '1')}
className={inputClass (invalid)}>
<option value="">&nbsp;</option>
<option value="0">有効</option>
<option value="1">廃止</option>
<option value="0" lang={message ('common:states.active').language}>
{message ('common:states.active').text}
</option>
<option value="1" lang={message ('common:states.deprecated').language}>
{message ('common:states.deprecated').text}
</option>
</select>)}
</FormField>
@@ -340,13 +349,14 @@ const TagListPage: FC = () => {
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded">
検索
{label ('common:search')}
</button>
</div>
</form>
</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="overflow-x-auto">
<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">
<SortHeader<FetchTagsOrderField>
by="name"
label="タグ"
label={label ('title')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField>
by="post_count"
label="件数"
label={label ('common:countLabel')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField>
by="category"
label="カテゴリ"
label={label ('common:categoryLabel')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">状態</th>
<th className="p-2 text-left whitespace-nowrap">別名</th>
<th className="p-2 text-left whitespace-nowrap">上位タグ</th>
<th className="p-2 text-left whitespace-nowrap">
{label ('common:stateLabel')}
</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">
<SortHeader<FetchTagsOrderField>
by="created_at"
label="最初の記載日時"
label={label ('firstRecordedColumn')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
<th className="p-2 text-left whitespace-nowrap">
<SortHeader<FetchTagsOrderField>
by="updated_at"
label="更新日時"
label={label ('updatedAt')}
currentOrder={order}
defaultDirection={defaultDirection}/>
</th>
@@ -420,8 +436,12 @@ const TagListPage: FC = () => {
withCount={false}/>
</td>
<td className="p-2 text-right">{row.postCount}</td>
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
<td className="p-2">{tagStateLabel (row.deprecatedAt)}</td>
<td className="p-2">
{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.parents.map (t => (
@@ -432,14 +452,26 @@ const TagListPage: FC = () => {
withCount={false}/>
</span>))}
</td>
<td className="p-2">{dateString (row.createdAt)}</td>
<td className="p-2">{dateString (row.updatedAt)}</td>
<td className="p-2">
<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">
<PrefetchLink
to={row.category === 'nico'
? `/posts/changes?external_tag=${ row.id }`
: `/posts/changes?tag=${ row.id }`}>
耕作履歴
{label ('history')}
</PrefetchLink>
</td>
</tr>))}
@@ -448,7 +480,7 @@ const TagListPage: FC = () => {
</div>
<Pagination page={page} totalPages={totalPages}/>
</div>) : '結果ないよ(笑)')}
</div>) : (isError ? null : label ('empty')))}
</MainArea>)
}