From 22f4a0af40ab62c353067e0d8e1a7f7706396627 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 18 Oct 2026 19:03:02 +0900 Subject: [PATCH] #428 --- frontend/src/locales/ain-Kana/history.json | 12 +- frontend/src/locales/ja/history.json | 7 + frontend/src/pages/tags/TagHistoryPage.tsx | 165 +++++++++++++++++---- frontend/src/types.ts | 1 + 4 files changed, 152 insertions(+), 33 deletions(-) diff --git a/frontend/src/locales/ain-Kana/history.json b/frontend/src/locales/ain-Kana/history.json index 24b4ac9..4ba959c 100644 --- a/frontend/src/locales/ain-Kana/history.json +++ b/frontend/src/locales/ain-Kana/history.json @@ -5,7 +5,10 @@ "nameChanges.title", "nameChanges.script", "nameChanges.automatic", - "nameChanges.certified" + "nameChanges.certified", + "nameChanges.otherLanguages", + "nameChanges.changed", + "nameChanges.uncertified" ], "exportPath": "export_path", "postTitle": "$t(tags:history)", @@ -29,11 +32,18 @@ "confirmTag": "シロシ「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?", "restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。", "nameChanges": { + "otherLanguages": "他言語情報", + "changed": "(異動あり)", + "uncertified": "(未公証)", "title": "言語別の変更", "script": "文字体系", "automatic": "自動生成", "certified": "公証済み" }, + "languageNames": { + "ja": "日本語", + "ain": "ヤウンモシルン アイヌ イタㇰ" + }, "revert": "アホシピレ", "restored": "アホシピレ オケレ!", "restoreFailed": "アホシピレ カ エアイカㇷ゚……", diff --git a/frontend/src/locales/ja/history.json b/frontend/src/locales/ja/history.json index 5fac891..3d79308 100644 --- a/frontend/src/locales/ja/history.json +++ b/frontend/src/locales/ja/history.json @@ -21,11 +21,18 @@ "confirmTag": "タグ『{{name}}』を版 {{version}} に差戻します.\nよろしいですか?", "restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。", "nameChanges": { + "otherLanguages": "他言語情報", + "changed": "(異動あり)", + "uncertified": "(未公証)", "title": "言語別の変更", "script": "文字体系", "automatic": "自動生成", "certified": "公証済み" }, + "languageNames": { + "ja": "日本語", + "ain": "ヤウンモシルン アイヌ イタㇰ" + }, "revert": "差戻", "restored": "差戻しました.", "restoreFailed": "差戻に失敗……", diff --git a/frontend/src/pages/tags/TagHistoryPage.tsx b/frontend/src/pages/tags/TagHistoryPage.tsx index 296d84b..e118b25 100644 --- a/frontend/src/pages/tags/TagHistoryPage.tsx +++ b/frontend/src/pages/tags/TagHistoryPage.tsx @@ -1,9 +1,11 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useEffect } from 'react' +import { useEffect, useId, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import { useLocale } from '@/lib/useLocale' import PrefetchLink from '@/components/PrefetchLink' +import LocalisedText from '@/components/LocalisedText' import PageTitle from '@/components/common/PageTitle' import Pagination from '@/components/common/Pagination' import MainArea from '@/components/layout/MainArea' @@ -12,6 +14,7 @@ import PageHead from '@/components/common/PageHead' import { apiPut } from '@/lib/api' import { useDialogue } from '@/lib/dialogues/useDialogue' import { clientScrollBehaviour } from '@/lib/clientAnimation' +import { localeLanguage } from '@/lib/locales' import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys' import { fetchTagChanges } from '@/lib/tags' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' @@ -20,7 +23,60 @@ import { cn, localisedDateString } from '@/lib/utils' import type { FC, ReactNode } from 'react' -import type { TagNameSnapshot } from '@/types' +import type { TagNameSnapshot, TagVersion } from '@/types' + + +const normaliseNameSnapshot = (value: unknown): TagNameSnapshot | null => { + if (value == null || typeof value !== 'object' || Array.isArray (value)) + return null + const entry = value as Record + const automatic = entry.autoGenerated ?? entry.auto_generated + const script = entry.scriptCode ?? entry.script_code + const aliases = Array.isArray (entry.aliases) ? entry.aliases : [] + return { + name: typeof entry.name === 'string' ? entry.name : null, + scriptCode: typeof script === 'string' ? script : null, + autoGenerated: typeof automatic === 'boolean' ? automatic : null, + aliases: aliases.flatMap (alias => { + if (typeof alias === 'string') + return [{ name: alias, scriptCode: null }] + if (alias == null || typeof alias !== 'object' || typeof alias.name !== 'string') + return [] + const aliasScript = alias.scriptCode ?? alias.script_code + return [{ name: alias.name, + scriptCode: typeof aliasScript === 'string' ? aliasScript : null }] + }) } +} + + +const snapshotNames = (names: TagVersion['names']): Record => { + try + { + const entries = typeof names === 'string' ? JSON.parse (names) : names + if (entries == null || typeof entries !== 'object' || Array.isArray (entries)) + return { } + return Object.fromEntries (Object.entries (entries).flatMap (([language, value]) => { + const snapshot = normaliseNameSnapshot (value) + return snapshot == null ? [] : [[language, snapshot]] + })) + } + catch + { + return { } + } +} + + +const renderPrimaryName = ( + snapshot: TagNameSnapshot, + language: string, + label: (key: string) => ReactNode, +) => ( + <> + {snapshot.name} + {snapshot.name != null && snapshot.autoGenerated === true + && label ('nameChanges.uncertified')} + ) const renderNameSnapshot = ( @@ -31,36 +87,30 @@ const renderNameSnapshot = (
{label ('name')}: - {snapshot.name} + {renderPrimaryName (snapshot, language, label)}
- {snapshot.scriptCode != null && ( -
{label ('nameChanges.script')}:{snapshot.scriptCode}
)} {snapshot.aliases.length > 0 && (
{label ('tags:aliases')}: {snapshot.aliases.map (alias => ( {alias.name} - {alias.scriptCode != null && ({alias.scriptCode})} ))}
)} - {snapshot.autoGenerated != null && ( -
- {label (snapshot.autoGenerated ? 'nameChanges.automatic' : 'nameChanges.certified')} -
)}
) -const renderDiff = ( - diff: { current: string | null; prev: string | null }, - format: (value: string) => ReactNode = value => value, +const renderDiff = ( + diff: { current: T | null; prev: T | null }, + format: (value: T) => ReactNode, + equal: (prev: T | null, current: T | null) => boolean = Object.is, ) => ( <> - {diff.prev !== diff.current + {!(equal (diff.prev, diff.current)) ? ( <> - {diff.prev && <>{format (diff.prev)}
} + {diff.prev != null && diff.prev !== '' && <>{format (diff.prev)}
}
{diff.current != null && format (diff.current)} @@ -70,6 +120,73 @@ const renderDiff = ( ) +const TagVersionNames = ({ change }: { change: TagVersion }) => { + const locale = useLocale () + const language = localeLanguage (locale) + const { message, label } = useUITranslation ('history') + const { i18n } = useTranslation ('history') + const [expanded, setExpanded] = useState (false) + const panelId = useId () + const names = snapshotNames (change.names) + const nameChanges = (change.nameChanges ?? []).map (entry => ({ + ...entry, + current: normaliseNameSnapshot (entry.current), + prev: normaliseNameSnapshot (entry.prev) })) + const selected = nameChanges.find (entry => entry.languageCode === language) + const legacyName = (name: string | null): TagNameSnapshot | null => + name == null ? null : { + name, autoGenerated: null, scriptCode: null, aliases: [] } + const current = + selected != null + ? selected.current + : names[language] ?? legacyName (change.name.current) + const previous = + selected != null + ? selected.prev + : ((change.nameChanges != null && change.name.prev === change.name.current) + ? current : legacyName (change.name.prev)) + const otherLanguages = [...new Set ([...Object.keys (names), + ...nameChanges.map (entry => entry.languageCode)])] + .filter (code => code !== language).sort () + const otherChanges = nameChanges.filter (entry => entry.languageCode !== language) + + return ( + <> + {renderDiff ({ current, prev: previous }, + snapshot => renderPrimaryName (snapshot, language, label), + (prev, next) => prev?.name === next?.name + && prev?.autoGenerated === next?.autoGenerated)} + {otherLanguages.length > 0 && ( +
+ + +
)} + ) +} + + const TagHistoryPage: FC = () => { const { message, label } = useUITranslation ('history') const dialogue = useDialogue () @@ -150,23 +267,7 @@ const TagHistoryPage: FC = () => { className={cn ('even:bg-gray-100 dark:even:bg-gray-700')}> {change.tagId}.{change.versionNo} - {renderDiff (change.name)} - {(change.nameChanges?.length ?? 0) > 0 && ( -
-
{label ('nameChanges.title')}
- {change.nameChanges?.map (entry => ( -
- {entry.languageCode} - {entry.prev != null && ( -
- {renderNameSnapshot (entry.prev, entry.languageCode, label)} -
)} - {entry.current != null && ( -
- {renderNameSnapshot (entry.current, entry.languageCode, label)} -
)} -
))} -
)} + {renderDiff (change.category, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 794b565..8dbfb09 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -382,6 +382,7 @@ export type TagVersion = { tagId: number versionNo: number eventType: 'create' | 'update' | 'discard' | 'restore' + names?: Record | string | null nameChanges?: TagNameChange[] name: { current: string | null; prev: string | null } category: { current: Category; prev: Category | null }