diff --git a/backend/app/services/counts_condition.rb b/backend/app/services/counts_condition.rb index 22931bf..1104c0f 100644 --- a/backend/app/services/counts_condition.rb +++ b/backend/app/services/counts_condition.rb @@ -340,7 +340,7 @@ class CountsCondition return @definition.alias_source(root) if root.start_with?('aliases_') return [nil, @definition.name_expression(root)] end - return [nil, "(#{ @population.latest_sql })"] if root == 'latest' + return [nil, "(#{ @population.current_latest_sql })"] if root == 'latest' if root == 'user' && parts.any? expression = parts.first == 'id' ? 'v.created_by_user_id' : '(SELECT actor.name FROM users actor WHERE actor.id = v.created_by_user_id)' diff --git a/backend/app/services/counts_defaults.rb b/backend/app/services/counts_defaults.rb index a29fe7b..8c41628 100644 --- a/backend/app/services/counts_defaults.rb +++ b/backend/app/services/counts_defaults.rb @@ -20,7 +20,7 @@ class CountsDefaults details = details.transform_values do |value| value.split(' ').flat_map do |token| if resource == 'tags' && ['name', 'parents.name'].include?(token) - token == 'name' ? name : ["parents.#{ name }", "parents.#{ aliases }"] + token == 'name' ? name : "parents.#{ name }" elsif resource == 'tags' && token == 'aliases' aliases elsif resource == 'tags' && token.start_with?('aliases.') diff --git a/backend/app/services/counts_population.rb b/backend/app/services/counts_population.rb index cea8d78..3776444 100644 --- a/backend/app/services/counts_population.rb +++ b/backend/app/services/counts_population.rb @@ -41,8 +41,7 @@ class CountsPopulation raise ArgumentError end - def latest_sql(row = 'v', cutoff = nil) - cutoff ||= history ? at : timestamp(params['at']) || at + def latest_at_sql(row, cutoff) keys = definition.keys.map { "newer.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ') bound = definition.model.connection.quote(cutoff) <<~SQL.squish @@ -52,6 +51,14 @@ class CountsPopulation SQL end + def current_latest_sql(row = 'v') + keys = definition.keys.map { "newer.#{ _1 } = #{ row }.#{ _1 }" }.join(' AND ') + <<~SQL.squish + NOT EXISTS (SELECT 1 FROM #{ definition.table } newer + WHERE #{ keys } AND newer.version_no > #{ row }.version_no) + SQL + end + def changed_sql(key) column = definition.column(key) "(previous.id IS NOT NULL AND NOT (v.#{ column } <=> previous.#{ column }))" @@ -125,7 +132,7 @@ class CountsPopulation raise ArgumentError if point > at @scope = @scope.where("v.#{ definition.time_column } <= ?", point) - .where(Arel.sql(latest_sql('v', point))).where.not('v.event_type' => 'discard') + .where(Arel.sql(latest_at_sql('v', point))).where.not('v.event_type' => 'discard') return end first = timestamp(params['from']) diff --git a/backend/app/services/counts_table_query.rb b/backend/app/services/counts_table_query.rb index f2c52f6..8585c36 100644 --- a/backend/app/services/counts_table_query.rb +++ b/backend/app/services/counts_table_query.rb @@ -1,5 +1,7 @@ # Common entrypoint for six version resources; legacy endpoints keep their contracts. class CountsTableQuery + SQL_HINTS = ['MAX_EXECUTION_TIME(10000)', + "SET_VAR(optimizer_switch='semijoin=off')"].freeze MAX_COLUMNS = 100 class TooManyColumns < ArgumentError; end @@ -127,7 +129,7 @@ class CountsTableQuery numbered AS (SELECT row_groups.*, ROW_NUMBER() OVER (ORDER BY #{ order_rows.join(', ') }) AS position FROM row_groups), group_totals AS (SELECT COUNT(*) AS group_count FROM row_groups) - SELECT /*+ MAX_EXECUTION_TIME(10000) */ cells.*, + SELECT /*+ #{ SQL_HINTS.join(' ') } */ cells.*, page_rows.position AS counts_position, group_totals.group_count AS counts_group_count FROM group_totals LEFT JOIN numbered page_rows ON page_rows.position > #{ (page - 1) * limit } @@ -175,7 +177,7 @@ class CountsTableQuery def details query = population.scope.select(Arel.sql( - "(#{ population.latest_sql }) AS counts_latest")) + "(#{ population.current_latest_sql }) AS counts_latest")) order = sort_sql { detail_expression(_1) } query = query.order(Arel.sql((order + ['v.id ASC']).join(', '))) .limit(limit).offset((page - 1) * limit) @@ -253,11 +255,11 @@ class CountsTableQuery if HistoryAccess.admin? || mode == 'summary' total = population.scope.except(:select) - .optimizer_hints('MAX_EXECUTION_TIME(10000)').count + .optimizer_hints(*SQL_HINTS).count return @record_counts = { total:, visible: total, denied: 0 } end total, denied = unrestricted_population.scope.except(:select) - .optimizer_hints('MAX_EXECUTION_TIME(10000)').pick(Arel.sql('COUNT(*)'), + .optimizer_hints(*SQL_HINTS).pick(Arel.sql('COUNT(*)'), Arel.sql("COALESCE(SUM(CASE WHEN #{ HistoryAccess.sql(@definition.model, 'v') } " + 'THEN 0 ELSE 1 END), 0)')) # Related/previous-version predicates may differ under public authority. @@ -267,7 +269,7 @@ class CountsTableQuery total.to_i - denied.to_i else population.scope.except(:select) - .optimizer_hints('MAX_EXECUTION_TIME(10000)').count + .optimizer_hints(*SQL_HINTS).count end @record_counts = { total: total.to_i, denied: denied.to_i, visible: } end @@ -309,7 +311,7 @@ class CountsTableQuery prefix = "WITH cells AS (#{ sql })" @connection.select_value(<<~SQL).to_i #{ prefix } - SELECT /*+ MAX_EXECUTION_TIME(10000) */ COUNT(*) FROM ( + SELECT /*+ #{ SQL_HINTS.join(' ') } */ COUNT(*) FROM ( SELECT #{ keys.join(', ') } FROM cells GROUP BY #{ keys.join(', ') } ) output_groups SQL @@ -335,7 +337,7 @@ class CountsTableQuery end values = @connection.select_rows(<<~SQL) #{ prefix } - SELECT /*+ MAX_EXECUTION_TIME(10000) */ DISTINCT #{ keys.join(', ') } FROM cells + SELECT /*+ #{ SQL_HINTS.join(' ') } */ DISTINCT #{ keys.join(', ') } FROM cells ORDER BY #{ order } LIMIT #{ MAX_COLUMNS + 1 } SQL raise TooManyColumns if values.length > MAX_COLUMNS @@ -395,7 +397,7 @@ class CountsTableQuery end def detail_expression(key) - return "(#{ population.latest_sql })" if key == 'latest' + return "(#{ population.current_latest_sql })" if key == 'latest' return @definition.name_expression(key) if @definition.resource == 'tags' && @definition.name_column(key) aliases = { 'tags' => 'tags_json', 'parents' => @definition.resource == 'posts' ? diff --git a/backend/spec/requests/counts_table_spec.rb b/backend/spec/requests/counts_table_spec.rb index 8b4a115..5c54067 100644 --- a/backend/spec/requests/counts_table_spec.rb +++ b/backend/spec/requests/counts_table_spec.rb @@ -91,6 +91,34 @@ RSpec.describe 'Counts table API', type: :request do expect(versions(query(predicate.merge('conditions.0.value' => 'true')))).to eq([3]) end + it 'distinguishes the snapshot cutoff from the current latest flag and condition' do + past = { definition: 'latest', at: '2024-01-31T16:00:00Z' } + result = query(past) + expect(versions(result)).to eq([2]) + expect(result['versions'].first['latest']).to be(false) + predicate = { conditions: '', 'conditions.0.field' => 'latest', + 'conditions.0.operator' => 'eq', 'conditions.0.value' => 'false' } + expect(versions(query(past.merge(predicate)))).to eq([2]) + expect(versions(query(past.merge(predicate).merge('conditions.0.value' => 'true')))) + .to eq([]) + unchanged = query(definition: 'latest', at: '2024-02-01T00:00:00Z') + expect(versions(unchanged)).to eq([3]) + expect(unchanged['versions'].first['latest']).to be(true) + expect(versions(query(sort: 'latest:desc resource_id:asc version_no:asc'))).to eq([3, 1, 2]) + end + + it 'checks the current latest flag beyond the execution timestamp' do + PostVersion.create!(post: record, version_no: 4, event_type: 'update', + url: record.url, title: 'after execution', tags: '', tags_json: [], + parent_post_ids: '', created_at: Time.utc(2025, 2, 1)) + result = query + expect(versions(result)).to eq([1, 2, 3]) + expect(result['versions'].map { _1['latest'] }).to eq([false, false, false]) + snapshot = query(definition: 'latest') + expect(versions(snapshot)).to eq([3]) + expect(snapshot['versions'].first['latest']).to be(false) + end + it 'keeps ANY, ALL and NONE distinct without multiplying parent records' do values = { conditions: '', 'conditions.0.field' => 'tags', 'conditions.0.values' => '1', condition_values: '', diff --git a/docs/counts-release-checklist.md b/docs/counts-release-checklist.md index a6d9530..d0a7312 100644 --- a/docs/counts-release-checklist.md +++ b/docs/counts-release-checklist.md @@ -28,7 +28,7 @@ bundle exec rspec spec/requests/counts_table_spec.rb spec/requests/counts_downlo frontend ディレクトリで提案する対象(未実行): ```sh -npm run test:run -- src/lib/counts.test.ts src/lib/api.test.ts src/pages/CountsResourcePage.test.tsx src/pages/help/HelpChangesPage.test.tsx src/components/counts/CountsControls.test.tsx src/components/counts/CountsSummaryResults.test.tsx src/components/counts/CountsExportProgress.test.tsx +npm run test:run -- src/lib/counts.test.ts src/lib/api.test.ts src/pages/counts/CountsResourcePage.test.tsx src/pages/help/HelpChangesPage.test.tsx src/components/counts/CountsControls.test.tsx src/components/counts/CountsSummaryResults.test.tsx src/components/counts/CountsExportProgress.test.tsx ``` SSR は frontend/server.ssr.test.mjs が dist/server/entry-server.js を読み込む。 @@ -104,7 +104,7 @@ SQL の速度改善・索引追加は別 Issue とし、今回 migration は追 - frontend/src/components/counts/CountsSummaryResults.tsx:行番号コードの撤去、私的セルの ドリルダウン抑止、公開列のソート維持。 - frontend/src/components/counts/CountsSortHeader.tsx:禁止座標のソート抑止と伏字の理由表示。 -- frontend/src/pages/CountsResourcePage.tsx:専用エラーと、初回失敗後にも再実行できる状態。 +- frontend/src/pages/counts/CountsResourcePage.tsx:専用エラーと、初回失敗後にも再実行できる状態。 - frontend/src/locales/ja/counts.json:承認されたエラー文言。 - frontend/src/locales/ain-Kana/counts.json:同じキーと未翻訳・日本語 fallback の管理。 @@ -121,7 +121,7 @@ SQL の速度改善・索引追加は別 Issue とし、今回 migration は追 再描画、開始受付失敗、通信断、独立期限、取消の競合・再試行、unmount、iframe 失敗。 - frontend/src/components/counts/CountsSummaryResults.test.tsx(新規):伏字・理由・コピー・旧行番号。 - frontend/src/components/counts/CountsControls.test.tsx:summary 行番号廃止と表示件数選択。 -- frontend/src/pages/CountsResourcePage.test.tsx:初期エラーからの再実行、サイト名付き title。 +- frontend/src/pages/counts/CountsResourcePage.test.tsx:初期エラーからの再実行、サイト名付き title。 - frontend/src/lib/api.test.ts:共有 CSRF 待機中の中断、iframe の解放・重複失敗通知。 - frontend/src/lib/counts.test.ts:理由別エラーと任意のサーバー文字列を表示しないこと。 - frontend/src/test/setup.ts:時刻が進む非同期 RAF と Canvas のテスト専用 fallback。 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 044ffb0..a6799ad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -81,7 +81,7 @@ const MaterialListPage = lazyPage (() => import ('@/pages/materials/MaterialList const MaterialNewPage = lazyPage (() => import ('@/pages/materials/MaterialNewPage')) const MaterialSyncSuppressionsPage = lazyPage ( () => import ('@/pages/materials/MaterialSyncSuppressionsPage')) -const CountsResourcePage = lazyPage (() => import ('@/pages/CountsResourcePage')) +const CountsResourcePage = lazyPage (() => import ('@/pages/counts/CountsResourcePage')) const MorePage = lazyPage (() => import ('@/pages/MorePage')) const GekanatorUnavailablePage = lazyPage (() => import ('@/pages/GekanatorUnavailablePage')) const NicoTagListPage = lazyPage (() => import ('@/pages/tags/NicoTagListPage')) diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index ffda722..22dc2dd 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -120,7 +120,8 @@ export const menuOutline = ( { ...message ('entertainment'), visible: false, subMenu: [ { ...message ('theatre'), to: '/theatres/1' }, { ...message ('gekanator'), to: '/gekanator' }] }, - { ...message ('counts'), visible: false, subMenu: [ + { ...message ('counts'), + name: `${ message ('counts').name } (β)`, visible: false, subMenu: [ { ...message ('posts'), to: '/counts/posts' }, { ...message ('tags'), to: '/counts/tags' }, { ...message ('externalTags'), to: '/counts/nico-tags' }] }, diff --git a/frontend/src/components/counts/CountsAxes.tsx b/frontend/src/components/counts/CountsAxes.tsx index 89b0816..d878d86 100644 --- a/frontend/src/components/counts/CountsAxes.tsx +++ b/frontend/src/components/counts/CountsAxes.tsx @@ -49,8 +49,9 @@ const CountsAxes = ({ params, update }: { commit (next) } return
- {label (id === 'available' ? 'axisItems' : id)} + renderGroup={(id, content, over) =>
+ + {label (id === 'available' ? 'axisItems' : id)}
{content}
} renderItem={(id, group) => { diff --git a/frontend/src/index.css b/frontend/src/index.css index 581256e..39a3883 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -14,7 +14,7 @@ .ainu-ui-word { - display: inline-block; + display: inline; white-space: nowrap !important; word-break: keep-all !important; overflow-wrap: normal !important; diff --git a/frontend/src/locales/ain-Kana/common.json b/frontend/src/locales/ain-Kana/common.json index 2c72714..ad939ef 100644 --- a/frontend/src/locales/ain-Kana/common.json +++ b/frontend/src/locales/ain-Kana/common.json @@ -1,8 +1,8 @@ { - "_untranslated": ["languageNames.en"], "languageNames": { "ja": "シサㇺ イタㇰ", - "ain": "ヤウンモシルン アイヌ イタㇰ" + "ain": "ヤウンモシルン アイヌ イタㇰ", + "en": "イングリㇱ イタㇰ" }, "loadingLabel": "$t(common:loading)", "countLabel": "チピㇱキㇷ゚", diff --git a/frontend/src/locales/ain-Kana/counts.json b/frontend/src/locales/ain-Kana/counts.json index 8ea919a..2bffce5 100644 --- a/frontend/src/locales/ain-Kana/counts.json +++ b/frontend/src/locales/ain-Kana/counts.json @@ -1,28 +1,311 @@ { - "_untranslated": [ - "*", - "outputRows", - "maskedReason", - "downloadErrors", - "axisLimit", - "privateCondition", - "errors" - ], - "outputRows": "行数:{{value}} 件", - "maskedReason": "閲覧権限のない履歴に由来するため、この項目の値は表示できません。集計件数には含まれます。", - "downloadErrors": { - "unavailable": "ダウンロードの認可が失われたか、ファイルの有効期限が切れています。", - "failed": "ダウンロード応答を開始できませんでした。", - "timeout": "ダウンロード応答の開始確認がタイムアウトしました。" + "titles": { + "posts": "ミンタㇻ ウコピㇱキ", + "tags": "シロシ ウコピㇱキ", + "nico_tags": "ソイタ シロシ ウコピㇱキ", + "tag_descriptions": "シロシ オルㇱペ ウコピㇱキ ネワ オルㇱペ", + "tag_groups": "シロシ ウコ イソネカ ウコピㇱキ ネワ オルㇱペ", + "materials": "チカㇻペ ウコピㇱキ ネワ オルㇱペ" }, - "axisLimit": "集計軸は最大{{max}}個です。行・列の項目を減らしてください。", - "privateCondition": "非公開情報を保護するため、この抽出条件では集計できません。", + "target": "ウコピㇱキ イアヌ", + "localeName": "レ({{language}})", + "localeNameWithScript": "レ({{language}}・{{script}})", + "localeAliases": "モㇱマ レ({{language}})", + "localeAliasesWithScript": "モㇱマ レ({{language}}・{{script}})", + "run": "ウコピㇱキ キ", + "columnLimit": { + "title": "コラム チピㇱキㇷ゚ イヨッタ ポロ ネ", + "message": "ウコピㇱキ テーブル コラム {{max}} パㇰノ アエイワンケ エアㇱカイ。コラム イヌㇺケ シンナ カㇻ ネ ヤ、イフナラ クニ ペ ポンノ カㇻ ヤン。" + }, + "conditions": "ウコピㇱキ イアヌ", + "output": "アサンケ クニ オルㇱペ", + "filters": "イフナラ イアヌ", + "mode": "アヌカレ", + "exportProgress": { + "title": "エクスポート", + "format": "イソネカ:{{format}}", + "starting": "アオハシ コㇿ アン", + "queued": "アテレ コㇿ アン", + "running": "アカㇻ コㇿ アン", + "aggregating": "ウコピㇱキ コㇿ アン", + "writing": "アサンケ コㇿ アン", + "cancelling": "ソモ キ クス イカㇻ コㇿ アン", + "cancelled": "ソモ キ オケレ", + "ready": "チヌイェㇷ゚ アカㇻ オケレ・ダウンロード アオハシ(アウカオ オケレ ヤ ソモ アエラマン)", + "failed": "ハイタ", + "processed": "{{count}} ペ アカㇻ オケレ", + "measured": "{{processed}} / {{total}} ペ({{percent}}%)" + }, + "displaySettings": "アヌカレ イアヌ", + "summary": "ウコピㇱキ", + "details": "オルㇱペ", + "axis": "ウコピㇱキ アㇰシㇱ", + "axes": { + "month": "チュㇷ゚ ネ ウシンナ(UTC)", + "user": "$t(history:actor)", + "tags": "シロシ ネ ウシンナ" + }, + "metric": "フㇱコアンペ レコード チピㇱキㇷ゚", + "total": "フㇱコアンペ レコード チピㇱキㇷ゚:{{count}}", + "csv": "CSV", + "csvScope": "タネ アヌカレ ソㇱ ネワ アヌㇺケ コラム CSV ウン アサンケ", + "empty": "ネ フㇱコアンペ イサㇺ。", + "columns": { + "changedAt": "イカㇻ ト ヘㇺ トキ", + "version": "ウェㇽソン", + "name": "レ", + "resourceId": "Id.", + "postTitle": "レ", + "postURL": "URL", + "tags": "シロシ", + "deprecated": "エイワンケ ソモ エアㇱカイ シㇼキ", + "latest": "イヨッタ アシㇼ シロシ", + "thumbnailURL": "ポン ノカ モト URL", + "originalFrom": "ホㇱキ アオ ト ヘㇺ トキ(オㇿ ワノ)", + "originalBefore": "ホㇱキ アオ ト ヘㇺ トキ(エトコ ウン)", + "language": "イタㇰ", + "kind": "ウㇱケ オㇿ・ソイタ", + "sections": "シロシ アオ トキ ウシ", + "names": "イタㇰ ネ ウシンナ レ", + "thumbnailDisplay": "$t(shared:labels.thumbnail)" + }, + "fields": { + "from": "アオハシ(エコㇿ マシン トキ・オㇿ ワノ)", + "before": "オケレ(エコㇿ マシン トキ・エトコ ウン)", + "post": "ミンタㇻ Id.", + "tag": "シロシ レ ネ ヤ Id.", + "actor": "イカㇻ クㇽ Id.", + "limit": "アヌカレ ペ チピㇱキㇷ゚" + }, + "latestOnly": "イヨッタ アシㇼ ペ パテㇰ", + "truthValues": { + "true": "アニ", + "false": "ソモ" + }, + "boolean": { + "true": "ルウェ ウン", + "false": "ソモ ネ" + }, + "kinds": { + "internal": "ウㇱケ オㇿ シロシ", + "external": "ソイタ シロシ" + }, + "nested": { + "toggle": "テーブル オㇿ テーブル アㇱマカ・アセㇱケ", + "selection": "サパ ペ OFF ネ ヤッカ ポン ペ イヌㇺケ アウカオ コㇿ アン。", + "noColumns": "アサンケ クニ オルㇱペ ソモ アヌㇺケ。", + "asJSON": "オㇱケ テーブル JSON アニ アヌカレ" + }, + "sort": { + "action": "クリッㇰ コㇿ シネ ソート。Ctrl ネ ヤ タンネ アオサンケ コㇿ キー アオ・シンナ・アウㇱカ。NULL イヨㇱノ。", + "structured": "サパ テーブル オㇿ ポロンノ アン ペ アピㇱキ ワ、アウカオ ノカ イタㇰ シㇼ ネ アウコシㇼキ。Ctrl ネ ヤ タンネ アオサンケ コㇿ ポロンノ キー ソート エアㇱカイ。ポン テーブル ソート サパ テーブル ウン エラㇺシㇰ ソモ キ。", + "label": "{{column}}:{{direction}}、ホㇱキ ネ イキㇼ {{priority}}", + "none": "ソート イサㇺ", + "ascending": "ポン ペ ホㇱキ", + "descending": "ポロ ペ ホㇱキ", + "unsortedLabel": "{{column}}:ソート イサㇺ", + "singleLabel": "{{column}}:{{direction}}" + }, + "notRegistered": "レ ソモ アヌイェ", + "selection": { + "table": "ウコピㇱキ ネワ オルㇱペ テーブル", + "textMode": "イタㇰ イヌㇺケ コㇿ アン", + "hint": "クリッㇰ コㇿ セル アヌㇺケ、ドラッグ ネ ヤ Shift+クリッㇰ コㇿ ウシ アヌㇺケ。タッチ ネ セル タンネ アオサンケ ワ ドラッグ。Ctrl+C/Cmd+C コㇿ コピー。アㇻㇺプヤラ ケイ コㇿ オマン、Shift+アㇻㇺプヤラ ケイ コㇿ ウシ ポロ カㇻ、Escape コㇿ アウㇱカ。", + "copy": "コピー", + "copyHeaders": "サパ レ コㇿ コピー", + "row": "イキㇼ アヌㇺケ", + "column": "コラム アヌㇺケ", + "all": "オピッタ アヌㇺケ" + }, + "resize": { + "column": "{{column}} コラム サクマ シンナ。イキㇻ ドラッグ、タッチ ネ タンネ アオサンケ ワ ドラッグ。アㇻㇺプヤラ ケイ アニ カ シンナ エアㇱカイ。" + }, + "latestValue": "イヨッタ アシㇼ", + "sections": { + "range": "{{begin}}~{{end}}", + "separator": "、" + }, + "reorder": "{{column}}:アサンケ クニ オルㇱペ イキㇼ シンナ", + "state": { + "list": "シロシ ネ ウシンナ ミンタㇻ チピㇱキㇷ゚ イキㇼ", + "trend": "シロシ ネ ウシンナ ミンタㇻ チピㇱキㇷ゚ シンナ シㇼキ", + "details": "ネ トキ コㇿ オルㇱペ", + "history": "フㇱコアンペ ウコピㇱキ", + "historyDetails": "フㇱコアンペ オルㇱペ", + "at": "ウコピㇱキ トキ", + "boundary": "ウコピㇱキ トキ(UTC)", + "period": "トキ(UTC)", + "grain": "トキ イソネカ(UTC)", + "grains": { + "day": "ト ネ ウシンナ", + "month": "チュㇷ゚ ネ ウシンナ", + "year": "パ ネ ウシンナ" + }, + "postCount": "ミンタㇻ チピㇱキㇷ゚", + "totals": { + "posts": "ミンタㇻ チピㇱキㇷ゚ オピッタ", + "tags": "ウㇱケ オㇿ シロシ チピㇱキㇷ゚ オピッタ", + "assignments": "シロシ アオ ペ チピㇱキㇷ゚ オピッタ" + } + }, + "measure": "アピㇱキ ペ", + "export": "エクスポート", + "exportFormat": "エクスポート イソネカ", + "exportFormats": { + "json": "JSON", + "csv": "CSV" + }, + "resources": { + "posts": "ミンタㇻ", + "tags": "シロシ", + "nico_tags": "ソイタ シロシ", + "tag_descriptions": "シロシ オルㇱペ", + "tag_groups": "シロシ ウコ イソネカ", + "materials": "チカㇻペ" + }, + "latest": "イヨッタ アシㇼ", + "history": "フㇱコアンペ", + "referenceAt": "アㇱパ ト ヘㇺ トキ", + "referenceHelp": "エヌㇺケ トキ タ イヨッタ アシㇼ アン ペ アウコピㇱキ ワ アヌカレ。ソモ エヌイェ コㇿ タネ イヨッタ アシㇼ ペ アエイワンケ。", + "from": "アオハシ", + "through": "オケレ", + "axisItems": "オルㇱペ イキㇼ", + "rows": "イキㇼ", + "cols": "コラム", + "axisPlace": "{{field}} {{target}} ウン アオ", + "field": "オルㇱペ", + "operator": "オペレーター", + "add": "アオ", + "remove": "アウㇱカ", + "value": "アヌイェ ペ", + "blank": "エンプティ", + "additionalConditions": "モㇱマ イアヌ", + "changedField": "シンナ アカㇻ オルㇱペ", + "eventType": "シンナ イソネカ", + "count": "チピㇱキㇷ゚", + "outputTotal": "チピㇱキㇷ゚:{{value}} ペ", + "excludedRecords": "(アヌカㇻ エアイカㇷ゚ レコード {{count}} ペ アサンケ ソモ キ)", + "recordTotal": "アヌカㇻ エアㇱカイ ペ チピㇱキㇷ゚:{{count}}", + "grain": "トキ イソネカ", + "grains": { + "year": "パ", + "quarter": "パ 4 イキㇼ オㇿ シネ ペ", + "month": "チュㇷ゚", + "week": "シネ ウィーㇰ", + "day": "ト", + "hour": "トキ" + }, + "operatorSymbols": { + "in": "IN", + "not_in": "NOT IN", + "any": "ANY", + "all": "ALL", + "none": "NONE", + "eq": "=", + "ne": "≠", + "gt": ">", + "lt": "<", + "ge": "≥", + "le": "≤", + "like": "LIKE", + "range": "~" + }, + "operators": { + "in": "IN", + "not_in": "NOT IN", + "any": "アヌㇺケ ペ オㇿ シネ ペ カ アン", + "all": "アヌㇺケ ペ オピッタ アン", + "none": "アヌㇺケ ペ シネ ペ カ イサㇺ", + "eq": "ウコシノㇳ", + "ne": "ウコシノㇳ ソモ キ", + "gt": "イヨッタ ポロ", + "lt": "ポン ワ アン", + "ge": "ポロ ネ ヤ ウコシノㇳ", + "le": "ポン ネ ヤ ウコシノㇳ", + "like": "パターン", + "range": "ウシ" + }, + "pattern": { + "input": "ソㇱ アヌカレ イソネカ", + "single": "シネ ペ エヌイェ", + "multiple": "ポロンノ エヌイェ", + "list": "ポロンノ アヌㇺケ イキㇼ", + "pattern": "データ パターン", + "link": "データ ウコテㇷ゚", + "columns": "アヌカレ コラム", + "value": "アエイワンケ ペ", + "selection": "イヌㇺケ" + }, + "events": { + "create": "アシㇼ アカㇻ", + "update": "シンナ アカㇻ", + "discard": "アウㇱカ", + "restore": "アホシピレ" + }, + "attributes": { + "sourceLanguage": "イタㇰ シンナ カㇻ モト イタㇰ", + "source_language_code": "イタㇰ シンナ カㇻ モト イタㇰ", + "source_version_no": "モト ウェㇽソン チピㇱキㇷ゚", + "event_type": "シンナ イソネカ", + "body": "ヌイェㇷ゚", + "linked_tags": "ウコテㇷ゚ シロシ", + "description": "オルㇱペ", + "members": "ウコオマレ ペ", + "placements": "イアヌ ウシ", + "parent_id": "オナ チカㇻペ", + "tag_id": "シロシ", + "tag_name": "シロシ レ", + "tag_category": "シロシ イソネカ", + "discarded_at": "アウㇱカ ト ヘㇺ トキ", + "updated_at": "フㇱコアンペ イキㇼ アシㇼ アカㇻ ト ヘㇺ トキ", + "updated_by_user_id": "フㇱコアンペ イキㇼ アシㇼ カㇻ クㇽ", + "export_paths_json": "アサンケ パㇱ", + "file_blob_id": "チヌイェㇷ゚ Id.", + "file_filename": "チヌイェㇷ゚ レ", + "file_content_type": "チヌイェㇷ゚ イソネカ", + "file_byte_size": "チヌイェㇷ゚ サイズ", + "file_checksum": "チェックサム", + "file_sha256": "SHA-256", + "source_kind": "モト イソネカ", + "source_uri": "モト URI", + "source_path": "モト パㇱ", + "source_file_id": "モト チヌイェㇷ゚ Id.", + "normalized_source_key": "モト イアヌ キー", + "parent_post_ids": "サパ ミンタㇻ", + "parent_tag_ids": "サパ シロシ", + "video_ms": "チモイェノカ トキ", + "original_created_from": "ホㇱキ アカㇻ ト ヘㇺ トキ(オㇿ ワノ)", + "original_created_before": "ホㇱキ アカㇻ ト ヘㇺ トキ(エトコ ウン)", + "deprecated_at": "エイワンケ ソモ エアㇱカイ アカㇻ ト", + "section_name": "イキㇻ レ", + "children": "ポン ペ", + "profile": "アサンケ イアヌ", + "path": "アサンケ パㇱ" + }, + "rowNumberHeading": "No.", + "subtotal": "ポン ウコピㇱキ", + "singleTotal": "ウコピㇱキ オピッタ", + "grandTotal": "オピッタ ウコピㇱキ", + "rowNumbers": "イキㇼ チピㇱキㇷ゚", + "periodSeparator": "〜", + "bot": "$t(history:bot)", + "actorWithId": "{{name}} (#{{id}})", + "axisWithGrain": "{{field}}({{grain}})", + "outputRows": "イキㇼ チピㇱキㇷ゚:{{value}} ペ", + "maskedReason": "エヌカㇻ エアイカㇷ゚ フㇱコアンペ オㇿ ワ アウㇰ ペ クス、タン オルㇱペ アヌカレ エアイカㇷ゚。ウコピㇱキ チピㇱキㇷ゚ オㇿ アン。", + "downloadErrors": { + "unavailable": "ダウンロード エアㇱカイ イレンカ イサㇺ ネ ヤ、チヌイェㇷ゚ エイワンケ エアㇱカイ トキ オケレ。", + "failed": "ダウンロード ホㇱキ アオハシ カ エアイカㇷ゚。", + "timeout": "ダウンロード アオハシ アエヌカㇻ トキ オケレ。" + }, + "axisLimit": "ウコピㇱキ アㇰシㇱ {{max}} パㇰノ ネ。イキㇼ ネワ コラム オルㇱペ ポンノ カㇻ ヤン。", + "privateCondition": "ソモ アヌカレ オルㇱペ アシッケウ クス、タン イフナラ イアヌ アニ ウコピㇱキ エアイカㇷ゚。", "errors": { - "queryTimeout": "集計処理が時間制限を超えました。抽出条件や集計軸を減らして再実行してください。", - "workerUnavailable": "エクスポートワーカーが稼働していません。", - "stopped": "エクスポート処理が停止しました。再実行してください。", - "input": "入力条件が正しくありません。", - "permission": "この操作を実行する権限がありません。", - "authentication": "認証状態を確認して再実行してください。" + "queryTimeout": "ウコピㇱキ イカㇻ トキ オケレ。イフナラ イアヌ ネ ヤ ウコピㇱキ アㇰシㇱ ポンノ カㇻ ワ スイ キ ヤン。", + "workerUnavailable": "エクスポート ワーカー イカㇻ ソモ キ。", + "stopped": "エクスポート イカㇻ オケレ ソモ キ。スイ キ ヤン。", + "input": "エヌイェㇷ゚ シㇼキ ピㇼカ ソモ ネ。", + "permission": "タン イカㇻ キ クス イレンカ エコㇿ ソモ キ。", + "authentication": "エアヌカㇻ シㇼキ エヌカㇻ ワ スイ キ ヤン。" } } diff --git a/frontend/src/locales/ain-Kana/history.json b/frontend/src/locales/ain-Kana/history.json index 6eca4d7..f55fa4d 100644 --- a/frontend/src/locales/ain-Kana/history.json +++ b/frontend/src/locales/ain-Kana/history.json @@ -1,18 +1,5 @@ { - "_untranslated": [ - "exportPath", - "restoreScope", - "nameChanges.title", - "nameChanges.script", - "nameChanges.automatic", - "nameChanges.certified", - "nameChanges.otherLanguages", - "nameChanges.changed", - "nameChanges.currentReference", - "nameChanges.noNameRecord", - "nameChanges.uncertified" - ], - "exportPath": "export_path", + "exportPath": "アサンケ パㇱ", "postTitle": "$t(tags:history)", "tagTitle": "シロシ イヌイェㇷ゚ フㇱコアンペ", "materialTitle": "チカㇻペ フㇱコアンペ", @@ -32,17 +19,17 @@ "confirmTitle": "アホシピレ エヌカㇻ", "confirmPost": "「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?", "confirmTag": "シロシ「{{name}}」 ウェㇽソン {{version}} ウン アホシピレ ヤ?\nピㇼカ ヤ?", - "restoreScope": "名称・別名は現在表示中の言語だけを差し戻します。全言語の名称は復元しません。", + "restoreScope": "レ ネワ モㇱマ レ タネ アヌカレ イタㇰ パテㇰ アホシピレ。イタㇰ オピッタ コㇿ レ アホシピレ ソモ キ。", "nameChanges": { - "currentReference": "現行名・参考", - "noNameRecord": "この版には名称記録なし", - "otherLanguages": "他言語情報", - "changed": "(異動あり)", - "uncertified": "(未公証)", - "title": "言語別の変更", - "script": "文字体系", - "automatic": "自動生成", - "certified": "公証済み" + "currentReference": "タネ レ・アエヌカㇻ クニ ペ", + "noNameRecord": "タン ウェㇽソン タ レ アヌイェ ペ イサㇺ", + "otherLanguages": "モㇱマ イタㇰ オルㇱペ", + "changed": "(シンナ アカㇻ)", + "uncertified": "(ナ ソモ アヌカㇻ オケレ)", + "title": "イタㇰ ネ ウシンナ シンナ アカㇻ ペ", + "script": "ヌイェ イソネカ", + "automatic": "ヤイウㇰ アカㇻ", + "certified": "(アヌカㇻ オケレ)" }, "revert": "アホシピレ", "restored": "アホシピレ オケレ!", diff --git a/frontend/src/locales/ain-Kana/navigation.json b/frontend/src/locales/ain-Kana/navigation.json index 9d6d011..02e4509 100644 --- a/frontend/src/locales/ain-Kana/navigation.json +++ b/frontend/src/locales/ain-Kana/navigation.json @@ -24,5 +24,7 @@ "settings": "イアヌ", "legal": "イレンカ", "terms": "エイワンケ イレンカ", - "more": "モㇱマ アン ペ »" + "more": "モㇱマ アン ペ »", + "counts": "ウコピㇱキ", + "externalTags": "ソイタ シロシ" } diff --git a/frontend/src/locales/ain-Kana/shared.json b/frontend/src/locales/ain-Kana/shared.json index d7b0de8..3532d67 100644 --- a/frontend/src/locales/ain-Kana/shared.json +++ b/frontend/src/locales/ain-Kana/shared.json @@ -1,5 +1,7 @@ { - "head": { "description": "$t(posts:list.tagline)" }, + "head": { + "description": "$t(posts:list.tagline)" + }, "dialogue": { "confirm": "エヌカㇻ ワ ピㇼカ", "close": "$t(settings:actions.close)" @@ -56,7 +58,8 @@ "latest": "(イヨッタ アシㇼ)", "diff": "ウシンナイペ", "restore": "ホㇱピ", - "show": "ヌカㇻ" + "show": "ヌカㇻ", + "count": "チピㇱキㇷ゚" }, "messages": { "loading": "アウㇰ コㇿ アン…", diff --git a/frontend/src/locales/ja/common.json b/frontend/src/locales/ja/common.json index dc847d4..a806b3f 100644 --- a/frontend/src/locales/ja/common.json +++ b/frontend/src/locales/ja/common.json @@ -2,7 +2,7 @@ "languageNames": { "ja": "日本語", "ain": "北海道アイヌ語", - "en": "英語" + "en": "イングランド語" }, "loadingLabel": "Loading", "countLabel": "件数", diff --git a/frontend/src/locales/ja/counts.json b/frontend/src/locales/ja/counts.json index 4c9d687..5e6d66f 100644 --- a/frontend/src/locales/ja/counts.json +++ b/frontend/src/locales/ja/counts.json @@ -70,11 +70,11 @@ "thumbnailDisplay": "$t(shared:labels.thumbnail)" }, "fields": { - "from": "開始(端末の時刻帯・以上)", - "before": "終了(端末の時刻帯・未満)", - "post": "投稿 ID", - "tag": "タグ名または ID", - "actor": "操作者 ID", + "from": "開始(以上)", + "before": "終了(未満)", + "post": "投稿 Id.", + "tag": "タグ名または Id.", + "actor": "操作者 Id.", "limit": "表示件数" }, "latestOnly": "最新のみ", diff --git a/frontend/src/pages/CountsPage.tsx b/frontend/src/pages/CountsPage.tsx deleted file mode 100644 index 6be614d..0000000 --- a/frontend/src/pages/CountsPage.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import CountsResourcePage from '@/pages/CountsResourcePage' - - -const CountsPage = () => - -export default CountsPage diff --git a/frontend/src/pages/CountsResourcePage.test.tsx b/frontend/src/pages/counts/CountsResourcePage.test.tsx similarity index 99% rename from frontend/src/pages/CountsResourcePage.test.tsx rename to frontend/src/pages/counts/CountsResourcePage.test.tsx index 5fa7aa4..2ff2993 100644 --- a/frontend/src/pages/CountsResourcePage.test.tsx +++ b/frontend/src/pages/counts/CountsResourcePage.test.tsx @@ -6,7 +6,7 @@ import { HelmetProvider } from 'react-helmet-async' import { createMemoryRouter, RouterProvider, useLocation } from 'react-router-dom' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import CountsResourcePage from '@/pages/CountsResourcePage' +import CountsResourcePage from '@/pages/counts/CountsResourcePage' import DialogueProvider from '@/components/dialogues/DialogueProvider' import { countsKeys } from '@/lib/queryKeys' import SortableColumns from '@/components/common/SortableColumns' diff --git a/frontend/src/pages/CountsResourcePage.tsx b/frontend/src/pages/counts/CountsResourcePage.tsx similarity index 95% rename from frontend/src/pages/CountsResourcePage.tsx rename to frontend/src/pages/counts/CountsResourcePage.tsx index fb200b7..294d464 100644 --- a/frontend/src/pages/CountsResourcePage.tsx +++ b/frontend/src/pages/counts/CountsResourcePage.tsx @@ -222,7 +222,8 @@ const CountsResourceContent = ( } catch (failure) { - if (pendingRun.current !== pending) return + if (pendingRun.current !== pending) + return pendingRun.current = null setRunning (false) setRunFailure (failure) @@ -252,7 +253,8 @@ const CountsResourceContent = ( if (key === 'measures' || key.startsWith ('measures.')) next.delete (key) for (const [key, value] of Object.entries (extra)) next.set (key, value) } - if (key === 'sort') next.delete ('page') + if (key === 'sort') + next.delete ('page') setParams (next) } const download = async (format: 'csv' | 'json') => { @@ -321,8 +323,11 @@ const CountsResourceContent = ( aria-expanded={expanded.has (target)} aria-controls={`counts-definition-${target}`} onClick={() => setExpanded (previous => { const next = new Set (previous) - if (next.has (target)) next.delete (target) - else next.add (target) + if (next.has (target)) + next.delete (target) + else + next.add (target) + return next })}> -
    - {(['latest', 'history'] as const).map (definition => { - const next = countsDefinitionParams (definition, params) - const active = target === resource - && params.get ('definition') === definition - return
  • - {label (definition)}
  • - })} -
+
    + {(['latest', 'history'] as const).map (definition => { + const next = countsDefinitionParams (definition, params) + const active = target === resource + && params.get ('definition') === definition + return ( +
  • + + {label (definition)} + +
  • ) + })} +
)}