From 614a12e7aafd7571878658f60e85c593f7ae5cf3 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Wed, 28 Oct 2026 20:57:22 +0900 Subject: [PATCH] #434 --- backend/app/services/counts_defaults.rb | 12 +++ .../app/services/counts_export_projection.rb | 1 + .../services/counts_resource_definition.rb | 34 ++++++-- backend/app/services/counts_table_export.rb | 6 +- backend/app/services/counts_table_query.rb | 2 +- frontend/src/components/TopNav.tsx | 6 +- .../src/components/counts/CountsFieldTree.tsx | 64 +++++++------- frontend/src/lib/counts.ts | 86 +++++++++++++------ frontend/src/lib/countsDefinitions.ts | 6 +- frontend/src/lib/countsDisplay.ts | 19 ++-- frontend/src/lib/i18n.ts | 10 +++ frontend/src/lib/locales.ts | 3 - frontend/src/locales/ain-Kana/common.json | 5 ++ frontend/src/locales/ain-Kana/history.json | 4 - frontend/src/locales/ja/common.json | 5 ++ frontend/src/locales/ja/counts.json | 5 -- frontend/src/locales/ja/history.json | 4 - frontend/src/pages/tags/TagHistoryPage.tsx | 10 +-- 18 files changed, 175 insertions(+), 107 deletions(-) diff --git a/backend/app/services/counts_defaults.rb b/backend/app/services/counts_defaults.rb index e084a58..4c00efd 100644 --- a/backend/app/services/counts_defaults.rb +++ b/backend/app/services/counts_defaults.rb @@ -29,6 +29,18 @@ class CountsDefaults end end.join(' ') end + if ['posts', 'tags'].include?(resource) + fields = CountsResourceDefinition.new(resource).output_fields + selected = details.fetch('columns').split(' ') + details['columns'] = fields.map { _1[:key] }.select { selected.include?(_1) }.join(' ') + order = lambda do |items, prefix| + items.flat_map do |field| + path = [prefix, field[:key]].compact.join('.') + [path, *order.call(field.fetch(:children, []), path)] + end + end + details['order'] = order.call(fields, nil).join(' ') + end ['latest', 'history'].each do |definition| result << { definition:, mode: 'details', params: details } end diff --git a/backend/app/services/counts_export_projection.rb b/backend/app/services/counts_export_projection.rb index 87d5efe..e04df4c 100644 --- a/backend/app/services/counts_export_projection.rb +++ b/backend/app/services/counts_export_projection.rb @@ -165,6 +165,7 @@ class CountsExportProjection return @labels.fetch("kind:#{ value }", value) if key == 'kind' return @labels.fetch("category:#{ value }", value) if key == 'category' if ['language', 'script'].include?(key) && value.is_a?(Hash) + return @labels.fetch("language:#{ value[:code] }", value[:code]) if key == 'language' return @labels.fetch("#{ key }:#{ value[:code] }", value[:name] || @labels.fetch('notRegistered', '')) end diff --git a/backend/app/services/counts_resource_definition.rb b/backend/app/services/counts_resource_definition.rb index 872e379..5136653 100644 --- a/backend/app/services/counts_resource_definition.rb +++ b/backend/app/services/counts_resource_definition.rb @@ -115,13 +115,23 @@ class CountsResourceDefinition def self.aliases_key(code) = "aliases_#{ code.to_s.downcase.tr('-', '_') }" def name_columns - @name_columns ||= Locale.select(:code, :language_code, :script_code).order(:code) - .flat_map do |locale| - codes = { code: locale.code, language_code: locale.language_code, - script_code: locale.script_code } - [{ key: self.class.name_key(locale.code), label: 'counts:localeName', name_locale: codes }, - { key: self.class.aliases_key(locale.code), label: 'counts:localeAliases', - name_locale: codes, multiple_values: true }] + @name_columns ||= begin + locales = Locale.joins(:language) + .select('locales.code', 'locales.language_code', 'locales.script_code') + .order('languages.created_at ASC', 'languages.code ASC', 'locales.code ASC') + codes = locales.map do |locale| + { code: locale.code, language_code: locale.language_code, + script_code: locale.script_code } + end + names = codes.map do |locale| + { key: self.class.name_key(locale[:code]), label: 'counts:localeName', + name_locale: locale } + end + aliases = codes.map do |locale| + { key: self.class.aliases_key(locale[:code]), label: 'counts:localeAliases', + name_locale: locale, multiple_values: true } + end + names + aliases end end @@ -200,7 +210,7 @@ class CountsResourceDefinition keys.flat_map do |child| next [] if child == 'aliases' && key == 'tags' if child == 'name' && ['tags', 'parents'].include?(key) - next [{ key: child, label: CHILD_LABELS.fetch(child) }, *name_columns] + next name_columns end label = key == 'members' && child == 'tagId' ? 'counts:columns.resourceId' : CHILD_LABELS.fetch(child) @@ -218,6 +228,13 @@ class CountsResourceDefinition def condition_fields fields = output_fields if resource == 'tags' + fields = fields.map do |field| + next field unless field[:key] == 'parents' + + field.merge(children: field[:children] + [ + { key: 'name', label: 'history:name' }, + { key: 'names', label: 'counts:columns.names', children: child_fields('names') }]) + end fields += [{ key: 'name', label: 'counts:columns.name' }, { key: 'names', label: 'counts:columns.names', children: child_fields('names') }, { key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }] @@ -227,6 +244,7 @@ class CountsResourceDefinition next field unless field[:key] == 'tags' field.merge(children: field[:children] + [ + { key: 'name', label: 'history:name' }, { key: 'names', label: 'counts:columns.names', children: child_fields('names') }, { key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }]) end diff --git a/backend/app/services/counts_table_export.rb b/backend/app/services/counts_table_export.rb index 4096f3f..12c2625 100644 --- a/backend/app/services/counts_table_export.rb +++ b/backend/app/services/counts_table_export.rb @@ -58,7 +58,6 @@ class CountsTableExport def body(progress: nil, phase: nil) Enumerator.new do |output| ApplicationRecord.transaction(isolation: :repeatable_read) do - @language_names = Language.pluck(:code, :name).to_h processed = 0 started = false first = true @@ -256,6 +255,9 @@ class CountsTableExport def summary_value(value, index, page) field = @query.axes.all.fetch(index).fetch('field') return @labels.fetch('bot', '') if field == 'user' && value.nil? + if ['language_code', 'source_language_code'].include?(field) + return @labels.fetch("language:#{ value }", value) + end names = page[:axis_labels].fetch("a#{ index }", {}) name = field == 'user' ? names[value.to_s] : names.fetch(value.to_s, value) @@ -327,7 +329,7 @@ class CountsTableExport return @query.population.timezone.utc_to_local(value.utc).iso8601 end if key == 'language_code' || key == 'source_language_code' - return @language_names[value] + return @labels.fetch("language:#{ value }", value) end if value.is_a?(Hash) return value.to_h { |field, item| [@labels.fetch(camel(field.to_s), ''), csv_logical(field, item)] } diff --git a/backend/app/services/counts_table_query.rb b/backend/app/services/counts_table_query.rb index 258a9db..a33421f 100644 --- a/backend/app/services/counts_table_query.rb +++ b/backend/app/services/counts_table_query.rb @@ -391,7 +391,7 @@ class CountsTableQuery when 'user' User.where(id: values).pluck(:id, :name).to_h.transform_keys(&:to_s) when 'language_code', 'source_language_code' - Language.where(code: values).pluck(:code, :name).to_h + values.to_h { [_1, _1] } when 'tags' labels = tag_labels(values) @axis_names["a#{ index }"] = @tag_names diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 4904b96..7ffd24c 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -12,7 +12,9 @@ import TopNavUser from '@/components/TopNavUser' import TranslatedText from '@/components/TranslatedText' import { toast } from '@/components/ui/use-toast' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' +import { languageUIMessage } from '@/lib/i18n' import { isLocaleCode, + localeLanguage, localeFromPath, switchLocalePath, unlocalisedPath } from '@/lib/locales' @@ -132,7 +134,7 @@ export const menuOutline = ( const TopNav: FC = ({ user, locales, resolveLocaleSwitch }) => { const { message, label } = useUITranslation ('shared') const locale = useLocale () - const localeName = locales.find (entry => entry.code === locale)?.name + const localeName = languageUIMessage (localeLanguage (locale), locale).text const navigate = useLocaleNavigate () const location = useLocation () const currentLocation = useRef (location) @@ -447,7 +449,7 @@ const TopNav: FC = ({ user, locales, resolveLocaleSwitch }) => { }}> {locales.map (entry => isLocaleCode (entry.code) && ( ))} ) diff --git a/frontend/src/components/counts/CountsFieldTree.tsx b/frontend/src/components/counts/CountsFieldTree.tsx index 54a6d10..5665bf2 100644 --- a/frontend/src/components/counts/CountsFieldTree.tsx +++ b/frontend/src/components/counts/CountsFieldTree.tsx @@ -1,8 +1,8 @@ -import { useId, useState } from 'react' +import { useId, useLayoutEffect, useRef, useState } from 'react' import AnimatedCollapse from '@/components/common/AnimatedCollapse' import SortableList from '@/components/common/SortableList' -import { fieldsForCounts, orderedCountFields, +import { countChildPaths, fieldsForCounts, normaliseCountsParams, orderedCountFields, selectedCountChildren, selectedCountColumns, selectedCountFields } from '@/lib/counts' import { useCountsDefinition } from '@/lib/countsDefinitions' import { useClientMotion } from '@/lib/useClientMotion' @@ -22,12 +22,20 @@ const FieldBranch = ({ field, path, enabled, selected, params, select, update }: const [open, setOpen] = useState (false) const id = useId () const motion = useClientMotion () - const checked = selected.has (path) + const input = useRef (null) + const leaves = countChildPaths (field.children ?? [], path, true) + const count = leaves.filter (key => selected.has (key)).length + const checked = field.children == null ? selected.has (path) + : count > 0 && count === leaves.length + const partial = count > 0 && count < leaves.length + useLayoutEffect (() => { + if (input.current != null) input.current.indeterminate = partial + }, [partial]) return <>
@@ -89,48 +97,36 @@ const CountsFieldTree = ( update: (key: string, value: string) => void }, ) => { const definition = useCountsDefinition () + const resolved = normaliseCountsParams (resource, params, definition) const fields = suppliedFields ?? fieldsForCounts (resource, definition) - const roots = suppliedFields == null ? selectedCountColumns (resource, params, definition) - : selectedCountFields (fields, params, selectionKey, parent).map (field => field.key) - const selected = selectedCountChildren (params) + const roots = suppliedFields == null ? selectedCountColumns (resource, resolved, definition) + : selectedCountFields (fields, resolved, selectionKey, parent).map (field => field.key) + const selected = selectedCountChildren (resolved, fieldsForCounts (resource, definition)) const rootPath = (key: string) => parent ? `${parent}.${key}` : key for (const root of roots) selected.add (rootPath (root)) const select = (path: string, checked: boolean) => { - const root = fields.find (field => rootPath (field.key) === path) - if (root != null) + const next = new Set (selected) + const paths = countChildPaths (fields, parent) + .filter (key => key === path || key.startsWith (`${path}.`)) + for (const key of paths) { - const next = new Set (roots) - if (checked) next.add (root.key) - else next.delete (root.key) - update (selectionKey, fields.filter (field => next.has (field.key)) - .map (field => field.key).join (' ')) - return + if (checked) next.add (key) + else next.delete (key) } - if (checked) - { - const parts = path.split ('.') - for (let index = 1; index < parts.length; ++index) - selected.add (parts.slice (0, index + 1).join ('.')) - } - else selected.delete (path) - const nextParams = new URLSearchParams (params) - nextParams.set ('children', [...selected].filter (key => key.includes ('.')).join (' ')) - const children = selectedCountChildren (nextParams) + const nextParams = new URLSearchParams (resolved) + nextParams.set ('children', [...next].filter (key => key.includes ('.')).join (' ')) + const children = selectedCountChildren (nextParams, fieldsForCounts (resource, definition)) update ('children', [...children].join (' ')) - const nextRoots = new Set (roots) - const ancestor = fields.find (field => path.startsWith (`${rootPath (field.key)}.`)) - if (checked && ancestor != null) nextRoots.add (ancestor.key) - for (const field of fields) - if (field.children != null && !field.children.some (child => - children.has (`${rootPath (field.key)}.${child.key}`))) nextRoots.delete (field.key) - update (selectionKey, fields.filter (field => nextRoots.has (field.key)) + update (selectionKey, fields.filter (field => field.children == null + ? next.has (rootPath (field.key)) : field.children.some (child => + children.has (`${rootPath (field.key)}.${child.key}`))) .map (field => field.key).join (' ')) } const visibleFields = parent === '' ? fields.filter (field => field.key !== 'thumbnail') : fields return ( <> - ) } diff --git a/frontend/src/lib/counts.ts b/frontend/src/lib/counts.ts index 1c5a9ee..8baa06c 100644 --- a/frontend/src/lib/counts.ts +++ b/frontend/src/lib/counts.ts @@ -148,10 +148,8 @@ const nested: Partial> = { { key: 'id', label: 'counts:columns.resourceId' }, { key: 'versionNo', label: 'history:version' }, { key: 'kind', label: 'counts:columns.kind' }, - { key: 'name', label: 'history:name' }, { key: 'category', label: 'common:categoryLabel' }, - { key: 'sections', label: 'counts:columns.sections' }, - { key: 'names', label: 'counts:columns.names', children: nameFields }], + { key: 'sections', label: 'counts:columns.sections' }], names: nameFields, aliases: [{ key: 'languageCode', label: 'counts:columns.language' }, ...aliasFields], parents: [{ key: 'id', label: 'counts:columns.resourceId' }] } @@ -167,7 +165,8 @@ export const columnsForCounts = ( ? ['title', 'url', 'thumbnail_base', 'tags', 'parents', 'video_ms', 'original_created_from', 'original_created_before'] : (resource === 'tags' - ? ['name', 'names', 'category', 'aliases', 'parents', 'deprecated'] + ? [...(definition?.nameColumns?.map (field => field.key) ?? []), + 'category', 'parents', 'deprecated'] : (definition?.fields.filter (field => ![ 'resource_id', 'version_no', 'event_type', 'user', 'created_at'] .includes (field.key)).map (field => field.key) ?? []))), @@ -182,10 +181,8 @@ export const fieldsForCounts = ( key, label: (key === 'parents' && resource === 'tags') ? 'tags:parents' : (countColumns[key] ?? countsFieldLabel (key)), - children: key === 'parents' && resource === 'tags' - ? [...(nested.parents ?? []), { key: 'name', label: 'counts:columns.name' }, - { key: 'names', label: 'counts:columns.names', children: nameFields }] - : nested[key] })) + children: (key === 'tags' || (key === 'parents' && resource === 'tags')) + ? [...(nested[key] ?? []), ...(definition?.nameColumns ?? [])] : nested[key] })) export const orderedCountFields = ( @@ -235,7 +232,9 @@ export const selectedCountColumns = ( const common = ['resource_id', 'version_no', 'event_type', 'user', 'created_at'] const defaults: Record = { posts: ['title', 'url', 'tags', 'parents', 'video_ms'], - tags: ['name', 'category', 'aliases', 'parents'], + tags: [...(definition?.nameColumns ?? []).filter (field => + field.nameLocale?.code === currentViewingLocale ()).map (field => field.key), + 'category', 'parents'], nico_tags: ['name', 'linked_tags'], tag_descriptions: ['language_code', 'body', 'source_language_code'], tag_groups: ['name', 'description', 'members'], @@ -247,16 +246,39 @@ export const selectedCountColumns = ( } -export const countChildPaths = (fields: CountField[], prefix = ''): string[] => +export const countChildPaths = ( + fields: CountField[], prefix = '', leavesOnly = false, +): string[] => fields.flatMap (field => { const path = prefix ? `${prefix}.${field.key}` : field.key - return [path, ...countChildPaths (field.children ?? [], path)] + return [...((leavesOnly && field.children != null) ? [] : [path]), + ...countChildPaths (field.children ?? [], path, leavesOnly)] }) -export const selectedCountChildren = (params: URLSearchParams): Set => { +export const selectedCountChildren = ( + params: URLSearchParams, fields?: CountField[], +): Set => { const selected = new Set (params.has ('children') ? params.get ('children')!.split (' ') : ['tags.name', 'parents.id', 'aliases.name', 'members.tagId']) + if (fields != null) + { + const valid = new Set (countChildPaths (fields).filter (path => path.includes ('.'))) + for (const path of selected) if (!valid.has (path)) selected.delete (path) + const visit = (fields: CountField[], prefix = '') => { + for (const field of fields) + { + const path = prefix ? `${prefix}.${field.key}` : field.key + if (field.children == null) continue + visit (field.children, path) + if (prefix === '') continue + if (field.children.some (child => selected.has (`${path}.${child.key}`))) selected.add (path) + else selected.delete (path) + } + } + visit (fields) + return selected + } const clean = (fields: CountField[], prefix: string) => { for (const field of fields) { @@ -344,6 +366,15 @@ export const normaliseCountsParams = ( field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key const rootKey = (key: string) => (key === 'name' && current != null) ? current : ((key === 'aliases' && aliases != null) ? aliases : key) + const childKeys = (key: string): string[] => { + const [parent, field, ...rest] = key.split ('.') + if (parent !== 'tags' && (parent !== 'parents' || resource !== 'tags')) return [key] + if (field === 'name' && current != null) return [`${parent}.${current}`] + if (field === 'names') + return names.map (name => `${parent}.${name}`) + if (field === 'aliases' && aliases != null) return [`${parent}.${aliases}`] + return rest.length > 0 ? [] : [key] + } if (resource === 'tags' && params.has ('columns')) params.set ('columns', params.get ('columns')!.split (' ').flatMap (key => key === 'names' ? names : [rootKey (key)]) @@ -351,18 +382,12 @@ export const normaliseCountsParams = ( if (params.has ('children')) { const selected = params.get ('children')!.split (' ') - params.set ('children', selected.flatMap (key => { - if (['tags.names', 'parents.names'].includes (key)) - return names.map (name => `${key.slice (0, -6)}.${name}`) - if (['tags.aliases', 'parents.aliases'].includes (key) && aliases != null) - return [`${key.slice (0, -8)}.${aliases}`] - if (key.startsWith ('tags.aliases.') || key.startsWith ('parents.aliases.')) return [] - return key.startsWith ('tags.names.') || key.startsWith ('parents.names.') ? [] : [key] - }).join (' ')) + params.set ('children', [...new Set (selected.flatMap (childKeys))].join (' ')) } - if (resource === 'tags' && params.has ('order')) + if (params.has ('order')) params.set ('order', params.get ('order')!.split (' ').flatMap (key => - key === 'names' ? names : [rootKey (key)]) + key.includes ('.') ? childKeys (key) + : ((resource === 'tags') ? (key === 'names' ? names : [rootKey (key)]) : [key])) .join (' ')) } for (const [key, value] of params) @@ -419,16 +444,25 @@ export const normaliseCountsParams = ( } params.set (key, value) } - const children = selectedCountChildren (params) - if (params.has ('children')) params.set ('children', [...children].filter (Boolean).join (' ')) - if (params.has ('columns')) + const fields = fieldsForCounts (resource, definition) + const children = selectedCountChildren (params, definition == null ? undefined : fields) + if (params.has ('columns') && definition != null) { - const fields = fieldsForCounts (resource, definition) params.set ('columns', params.get ('columns')!.split (' ').filter (key => { const field = fields.find (field => field.key === key) return field != null && (field.children == null || field.children.some (child => children.has (`${key}.${child.key}`))) }).join (' ')) + const roots = new Set (params.get ('columns')!.split (' ')) + for (const path of children) + if (!roots.has (path.split ('.')[0])) children.delete (path) + } + if (params.has ('children')) params.set ('children', [...children].filter (Boolean).join (' ')) + if (params.has ('order') && definition != null) + { + const valid = new Set (countChildPaths (fields)) + params.set ('order', [...new Set (params.get ('order')!.split (' '))] + .filter (key => valid.has (key)).join (' ')) } return params } diff --git a/frontend/src/lib/countsDefinitions.ts b/frontend/src/lib/countsDefinitions.ts index acd6174..b8ce474 100644 --- a/frontend/src/lib/countsDefinitions.ts +++ b/frontend/src/lib/countsDefinitions.ts @@ -1,6 +1,6 @@ import { createContext, useContext } from 'react' -import { i18n, translateUIMessage } from '@/lib/i18n' +import { languageUIMessage, translateUIMessage } from '@/lib/i18n' import { apiGet } from '@/lib/api' import type { LocaleCode } from '@/lib/locales' @@ -52,9 +52,7 @@ const localiseCountsDefinition = ( ): CountsDefinition => { const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => { if (name == null) return null - const key = `languageNames.${name.languageCode}` - const language = i18n.exists (key, { ns: 'counts', lng: locale }) - ? translateUIMessage (key, 'counts', locale).text : name.languageCode + const language = languageUIMessage (name.languageCode, locale).text const scripts = new Set ((definition.nameColumns ?? []) .filter (field => field.nameLocale?.languageCode === name.languageCode) .map (field => field.nameLocale!.scriptCode.toLowerCase ())) diff --git a/frontend/src/lib/countsDisplay.ts b/frontend/src/lib/countsDisplay.ts index e541758..1ce16a0 100644 --- a/frontend/src/lib/countsDisplay.ts +++ b/frontend/src/lib/countsDisplay.ts @@ -1,7 +1,7 @@ import { fieldsForCounts, orderedCountFields, selectedCountChildren, sortedCountChildren } from '@/lib/counts' import { countsEventTypes, countsFieldLabel } from '@/lib/countsDefinitions' -import { translateUIMessage } from '@/lib/i18n' +import { languageUIMessage, translateUIMessage } from '@/lib/i18n' import { localisedDateString, msToTime } from '@/lib/utils' import type { CountColumn, CountField, CountRecord, CountsResult, @@ -133,15 +133,14 @@ export const countsLogicalName = ( const code = String (value) if (field === 'languageCode' || field === 'scriptCode') { - if (field === 'languageCode' && ['ja', 'ain'].includes (code)) - return translateUIMessage (`history:languageNames.${code}`, 'counts', locale) - const master = row[field === 'languageCode' ? 'languageName' : 'scriptName'] + if (field === 'languageCode') return languageUIMessage (code, locale) + const master = row.scriptName if (typeof master === 'string' && master.trim () !== '' && master.toLowerCase () !== code.toLowerCase ()) return { text: master } try { - const type = field === 'languageCode' ? 'language' : 'script' + const type = 'script' if (!displayNames.has (type)) displayNames.set (type, new Intl.DisplayNames (['ja'], { type, fallback: 'none' })) const text = displayNames.get (type)!.of (code) @@ -305,9 +304,9 @@ export const countsExportLabels = ( `common:categories.${key}`, 'counts', locale).text for (const key of ['true', 'false']) labels[`boolean:${key}`] = translateUIMessage (`boolean.${key}`, 'counts', locale).text - for (const key of ['ja', 'ain']) - labels[`language:${key}`] = translateUIMessage ( - `history:languageNames.${key}`, 'counts', locale).text + for (const key of new Set (['ja', 'ain', 'en', ...(definition?.nameColumns ?? []) + .map (field => field.nameLocale?.languageCode).filter (code => code != null)])) + labels[`language:${key}`] = languageUIMessage (key, locale).text labels.id = translateUIMessage ('columns.resourceId', 'counts', locale).text labels.name = translateUIMessage ('history:name', 'counts', locale).text for (const event of countsEventTypes) @@ -403,11 +402,13 @@ export const countsSummaryValue = ( const message = (key: string, values?: Record) => translateUIMessage (key, 'counts', locale, values).text if (value == null) return axis.field === 'user' ? message ('bot') : '' + if (['language_code', 'source_language_code'].includes (axis.field)) + return languageUIMessage (String (value), locale).text const logical = data.axisLabels?.[`a${index}`]?.[String (value)] if (axis.field === 'user') return message ('actorWithId', { name: logical || message ('notRegistered'), id: value }) if (logical != null) return logical - if (['tags', 'language_code', 'source_language_code'].includes (axis.field)) + if (axis.field === 'tags') return message ('notRegistered') if (axis.field === 'changed_field') return message (countsFieldLabel (String (value))) if (axis.field === 'event_type') return message (`events.${value}`) diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts index 2e8eb6f..53d0a41 100644 --- a/frontend/src/lib/i18n.ts +++ b/frontend/src/lib/i18n.ts @@ -111,6 +111,16 @@ export const translateUIMessage = ( } +export const languageUIMessage = ( + code: string, locale: string, instance: I18n = i18n, +): { text: string; language: string } => { + const key = `languageNames.${code}` + return instance.exists (key, { ns: 'common', lng: locale }) + ? translateUIMessage (key, 'common', locale, { }, instance) + : { text: code, language: 'und' } +} + + export const untranslatedKeys = ( locale: LocaleCode, namespace = 'navigation', diff --git a/frontend/src/lib/locales.ts b/frontend/src/lib/locales.ts index bf1b418..fd691e9 100644 --- a/frontend/src/lib/locales.ts +++ b/frontend/src/lib/locales.ts @@ -14,9 +14,6 @@ export const fetchLocales = (): Promise<{ locales: AvailableLocale[] }> => apiGet ('/locales') -export const LOCALE_LABELS: Record = { - ja: '日本語', 'ain-Kana': 'ヤウンモシルン アイヌ イタㇰ' } - export const isLocaleCode = (value: unknown): value is LocaleCode => SUPPORTED_LOCALES.some (locale => locale === value) diff --git a/frontend/src/locales/ain-Kana/common.json b/frontend/src/locales/ain-Kana/common.json index cb6642c..2c72714 100644 --- a/frontend/src/locales/ain-Kana/common.json +++ b/frontend/src/locales/ain-Kana/common.json @@ -1,4 +1,9 @@ { + "_untranslated": ["languageNames.en"], + "languageNames": { + "ja": "シサㇺ イタㇰ", + "ain": "ヤウンモシルン アイヌ イタㇰ" + }, "loadingLabel": "$t(common:loading)", "countLabel": "チピㇱキㇷ゚", "categoryLabel": "イソネカ", diff --git a/frontend/src/locales/ain-Kana/history.json b/frontend/src/locales/ain-Kana/history.json index c155dd4..6eca4d7 100644 --- a/frontend/src/locales/ain-Kana/history.json +++ b/frontend/src/locales/ain-Kana/history.json @@ -44,10 +44,6 @@ "automatic": "自動生成", "certified": "公証済み" }, - "languageNames": { - "ja": "シサㇺ イタㇰ", - "ain": "ヤウンモシルン アイヌ イタㇰ" - }, "revert": "アホシピレ", "restored": "アホシピレ オケレ!", "restoreFailed": "アホシピレ カ エアイカㇷ゚……", diff --git a/frontend/src/locales/ja/common.json b/frontend/src/locales/ja/common.json index 5497045..dc847d4 100644 --- a/frontend/src/locales/ja/common.json +++ b/frontend/src/locales/ja/common.json @@ -1,4 +1,9 @@ { + "languageNames": { + "ja": "日本語", + "ain": "北海道アイヌ語", + "en": "英語" + }, "loadingLabel": "Loading", "countLabel": "件数", "categoryLabel": "カテゴリ", diff --git a/frontend/src/locales/ja/counts.json b/frontend/src/locales/ja/counts.json index 6449e0e..6be0f0b 100644 --- a/frontend/src/locales/ja/counts.json +++ b/frontend/src/locales/ja/counts.json @@ -12,11 +12,6 @@ "localeNameWithScript": "名称({{language}}・{{script}})", "localeAliases": "別名({{language}})", "localeAliasesWithScript": "別名({{language}}・{{script}})", - "languageNames": { - "ja": "日本語", - "en": "英語", - "ain": "アイヌ語" - }, "run": "集計実行", "conditions": "集計条件", "output": "出力項目", diff --git a/frontend/src/locales/ja/history.json b/frontend/src/locales/ja/history.json index 46578e0..db307db 100644 --- a/frontend/src/locales/ja/history.json +++ b/frontend/src/locales/ja/history.json @@ -31,10 +31,6 @@ "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 4687ef2..37b75e7 100644 --- a/frontend/src/pages/tags/TagHistoryPage.tsx +++ b/frontend/src/pages/tags/TagHistoryPage.tsx @@ -15,6 +15,7 @@ import { apiPut } from '@/lib/api' import { useDialogue } from '@/lib/dialogues/useDialogue' import { clientScrollBehaviour } from '@/lib/clientAnimation' import { localeLanguage } from '@/lib/locales' +import { languageUIMessage } from '@/lib/i18n' import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys' import { fetchTagChanges } from '@/lib/tags' import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' @@ -150,7 +151,7 @@ const TagVersionRows = ({ change, children }: { children: (primary: ReactNode) => ReactNode }) => { const locale = useLocale () const language = localeLanguage (locale) - const { message, label } = useUITranslation ('history') + const { label } = useUITranslation ('history') const { i18n } = useTranslation ('history') const panelId = useId () const names = snapshotNames (change.names) @@ -210,15 +211,14 @@ const TagVersionRows = ({ change, children }: { )} {otherLanguages.map ((code, index) => { const entry = nameChanges.find (value => value.languageCode === code) - const key = `languageNames.${code}` - const known = i18n.exists (key, { ns: 'history', lng: locale }) + const language = languageUIMessage (code, locale, i18n) const next = entry != null ? entry.current : names[code] ?? null const prev = entry != null ? entry.prev : next return ( - - {known ? message (key).text : code} + + {language.text}