From 28e638e360695aa34d8fb5a410ee4eebd66543df Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 28 Sep 2026 00:30:03 +0900 Subject: [PATCH] #55 --- .../app/controllers/deerjikists_controller.rb | 7 ++- .../app/controllers/materials_controller.rb | 18 +++++-- .../app/controllers/nico_tags_controller.rb | 10 +++- backend/app/controllers/posts_controller.rb | 24 ++++++---- .../controllers/tag_versions_controller.rb | 8 ++-- backend/app/controllers/tags_controller.rb | 27 +++++++---- backend/app/models/tag_version.rb | 1 + .../app/representations/deerjikist_repr.rb | 8 ++-- backend/app/representations/material_repr.rb | 28 +++++------ backend/app/representations/post_repr.rb | 16 +++---- backend/app/services/post_create_preflight.rb | 1 + backend/app/services/post_import_previewer.rb | 9 ++-- backend/app/services/tag_version_recorder.rb | 25 +++++++--- ...0260922040000_add_names_to_tag_versions.rb | 36 ++++++++++++++ backend/db/schema.rb | 3 +- backend/spec/models/version_record_spec.rb | 4 ++ backend/spec/requests/tag_versions_spec.rb | 5 ++ .../services/tag_version_recorder_spec.rb | 47 +++++++++++++++++++ 18 files changed, 209 insertions(+), 68 deletions(-) create mode 100644 backend/db/migrate/20260922040000_add_names_to_tag_versions.rb create mode 100644 backend/spec/services/tag_version_recorder_spec.rb diff --git a/backend/app/controllers/deerjikists_controller.rb b/backend/app/controllers/deerjikists_controller.rb index 4d25cb6..e6cc209 100644 --- a/backend/app/controllers/deerjikists_controller.rb +++ b/backend/app/controllers/deerjikists_controller.rb @@ -1,5 +1,7 @@ class DeerjikistsController < ApplicationController def show + locale = resolve_locale! + platform = params[:platform].to_s.strip code = params[:code].to_s.strip return render_bad_request('platform は必須です.') if platform.blank? @@ -10,7 +12,7 @@ class DeerjikistsController < ApplicationController .includes(tag: :tag_names) .find_by(platform:, code:) if deerjikist - render json: DeerjikistRepr.base(deerjikist) + render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code) else head :not_found end @@ -20,6 +22,7 @@ class DeerjikistsController < ApplicationController return head :unauthorized unless current_user return head :forbidden unless current_user.gte_member? + locale = resolve_locale! platform = params[:platform].to_s.strip code = params[:code].to_s.strip tag_id = params[:tag_id].to_i @@ -32,7 +35,7 @@ class DeerjikistsController < ApplicationController d.save! end - render json: DeerjikistRepr.base(deerjikist) + render json: DeerjikistRepr.base(deerjikist, language_code: locale.language_code) end def destroy diff --git a/backend/app/controllers/materials_controller.rb b/backend/app/controllers/materials_controller.rb index 5a04654..f94f511 100644 --- a/backend/app/controllers/materials_controller.rb +++ b/backend/app/controllers/materials_controller.rb @@ -7,6 +7,8 @@ class MaterialsController < ApplicationController rescue_from MaterialZipExporter::MissingFileError, with: :render_zip_missing_file def index + locale = resolve_locale! + page = (params[:page].presence || 1).to_i limit = (params[:limit].presence || 20).to_i @@ -42,7 +44,10 @@ class MaterialsController < ApplicationController .offset(offset) .to_a - response = { materials: MaterialRepr.list_many(materials, host: request.base_url), + response = { materials: MaterialRepr.list_many( + materials, + host: request.base_url, + language_code: locale.language_code), count: } if filters[:tag_id] response[:tag_scope] = material_index_tag_scope(filters, tag_graph) @@ -55,6 +60,8 @@ class MaterialsController < ApplicationController end def show + locale = resolve_locale! + material = Material .includes(:tag, :material_export_items) @@ -65,7 +72,8 @@ class MaterialsController < ApplicationController wiki_page_body = material.tag&.tag_name('ja')&.wiki_page&.current_revision&.body - render json: MaterialRepr.base(material, host: request.base_url).merge(wiki_page_body:) + render json: MaterialRepr.base(material, host: request.base_url, + language_code: locale.language_code).merge(wiki_page_body:) end def create @@ -109,7 +117,8 @@ class MaterialsController < ApplicationController if material log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material)) - render json: MaterialRepr.base(material, host: request.base_url), status: :created + render json: MaterialRepr.base(material, host: request.base_url, + language_code: locale.language_code), status: :created else render_validation_error material end @@ -162,7 +171,8 @@ class MaterialsController < ApplicationController log_thumbnail_generation(material, MaterialThumbnailGenerator.generate!(material)) - render json: MaterialRepr.base(material, host: request.base_url) + render json: MaterialRepr.base(material, host: request.base_url, + language_code: locale.language_code) end def destroy diff --git a/backend/app/controllers/nico_tags_controller.rb b/backend/app/controllers/nico_tags_controller.rb index 03f17f2..64f74ab 100644 --- a/backend/app/controllers/nico_tags_controller.rb +++ b/backend/app/controllers/nico_tags_controller.rb @@ -1,5 +1,7 @@ class NicoTagsController < ApplicationController def index + locale = resolve_locale! + name = params[:name].presence linked_tag = params[:linked_tag].presence link_status = params[:link_status].presence @@ -68,7 +70,9 @@ class NicoTagsController < ApplicationController render json: { tags: tags.map { |tag| external_tag_json(tag).merge( recent_post_tag_created_at: tag.recent_post_tag_created_at, - linked_tags: tag.linked_tags.map { |lt| TagRepr.base(lt) }) + linked_tags: tag.linked_tags.map { |lt| + TagRepr.base(lt, language_code: locale.language_code) + }) }, count: } end @@ -109,7 +113,9 @@ class NicoTagsController < ApplicationController created_by_user: current_user) end - render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok + render json: tag.linked_tags.map { |t| + TagRepr.base(t, language_code: locale.language_code) + }, status: :ok rescue Tag::NicoTagNormalisationError render_validation_error fields: { tags: ['ニコニコ・タグ同士は連携できません.'] } rescue ActiveRecord::RecordInvalid => e diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index d88271b..99da557 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -92,7 +92,8 @@ class PostsController < ApplicationController q = q.except(:select, :order) render json: { posts: posts.map { |post| - PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json| + PostRepr.base(post, language_code: locale.language_code) + .merge(updated_at: post.updated_at_all).tap do |json| json['thumbnail'] = if post.thumbnail.attached? rails_storage_proxy_url(post.thumbnail, only_path: false) @@ -116,7 +117,7 @@ class PostsController < ApplicationController .first return head :not_found unless post - render json: PostRepr.base(post, current_user) + render json: PostRepr.base(post, current_user, language_code: locale.language_code) end def metadata @@ -191,6 +192,8 @@ class PostsController < ApplicationController end def show + locale = resolve_locale! + post = Post .includes(:uploaded_user, :parents, :children, @@ -209,8 +212,9 @@ class PostsController < ApplicationController parent_posts:, child_posts:, sibling_posts:, - related:) - .merge(tags: build_tag_tree_for(post)) + related:, + language_code: locale.language_code) + .merge(tags: build_tag_tree_for(post, locale.language_code)) end def create @@ -251,7 +255,7 @@ class PostsController < ApplicationController thumbnail: params[:thumbnail])).create!(locale) post.reload - render json: PostRepr.base(post), status: :created + render json: PostRepr.base(post, language_code: locale.language_code), status: :created rescue PostCreatePreflight::ValidationFailed => e render_validation_error fields: e.fields, base: e.base_errors rescue Tag::NicoTagNormalisationError @@ -379,8 +383,8 @@ class PostsController < ApplicationController return render json: conflict_json, status: :conflict if conflict_json post.reload - json = PostRepr.base(post, current_user) - json['tags'] = build_tag_tree_for(post) + json = PostRepr.base(post, current_user, language_code: locale.language_code) + json['tags'] = build_tag_tree_for(post, locale.language_code) render json:, status: :ok rescue Tag::NicoTagNormalisationError render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] } @@ -487,7 +491,7 @@ class PostsController < ApplicationController end end - def build_tag_tree_for post + def build_tag_tree_for post, language_code = 'ja' post_tags = post.post_tags.reject { |post_tag| post_tag.tag.deprecated? } tags = post_tags.map(&:tag) tag_ids = tags.map(&:id) @@ -517,7 +521,7 @@ class PostsController < ApplicationController sections = sections_by_tag_id.fetch(tag_id, []) if path.include?(tag_id) - return TagRepr.inline(tag).merge(children: [], sections:) + return TagRepr.inline(tag, language_code:).merge(children: [], sections:) end if memo.key?(tag_id) @@ -529,7 +533,7 @@ class PostsController < ApplicationController children = child_ids.filter_map { |cid| build_node.(cid, new_path) } - memo[tag_id] = TagRepr.inline(tag).merge(children:, sections:) + memo[tag_id] = TagRepr.inline(tag, language_code:).merge(children:, sections:) end internal_tags = root_ids.filter_map { |id| build_node.call(id, []) } diff --git a/backend/app/controllers/tag_versions_controller.rb b/backend/app/controllers/tag_versions_controller.rb index b861cfe..6709381 100644 --- a/backend/app/controllers/tag_versions_controller.rb +++ b/backend/app/controllers/tag_versions_controller.rb @@ -1,5 +1,6 @@ class TagVersionsController < ApplicationController def index + locale = resolve_locale! tag_id = params[:id].presence page = (params[:page].presence || 1).to_i limit = (params[:limit].presence || 20).to_i @@ -27,12 +28,12 @@ class TagVersionsController < ApplicationController .limit(limit) .offset(offset) - render json: { versions: serialise_versions(versions), count: } + render json: { versions: serialise_versions(versions, locale.language_code), count: } end private - def serialise_versions rows + def serialise_versions rows, language_code user_ids = rows.map(&:created_by_user_id).compact.uniq users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h @@ -54,13 +55,14 @@ class TagVersionsController < ApplicationController parent_tags = build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id) .map do |h| - { tag: TagRepr.base(tags_by_id[h[:tag_id]]), + { tag: TagRepr.base(tags_by_id[h[:tag_id]], language_code:), type: h[:type] } end { tag_id: row.tag_id, version_no: row.version_no, event_type: row.event_type, + names: row.names, name: { current: row.name, prev: row.attributes['prev_name'] }, category: { current: row.category, prev: row.attributes['prev_category'] }, deprecated_at: { current: row.deprecated_at&.iso8601, diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 643a548..53f738e 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -198,7 +198,8 @@ class TagsController < ApplicationController parent_tag_id = nil if parent_tag_id <= 0 material_filter = material_filter_param(default: 'any') - graph = build_with_depth_graph(material_filter) + locale = resolve_locale! + graph = build_with_depth_graph(material_filter, locale.language_code) tag_ids = if parent_tag_id @@ -325,7 +326,9 @@ class TagsController < ApplicationController return head :not_found unless tag render json: { tag: TagRepr.base(tag, language_code: locale.language_code), - deerjikists: DeerjikistRepr.many(tag.deerjikists) } + deerjikists: DeerjikistRepr.many( + tag.deerjikists, + language_code: locale.language_code) } end def deerjikists_by_name @@ -342,7 +345,9 @@ class TagsController < ApplicationController return head :not_found unless tag render json: { tag: TagRepr.base(tag, language_code: locale.language_code), - deerjikists: DeerjikistRepr.many(tag.deerjikists) } + deerjikists: DeerjikistRepr.many( + tag.deerjikists, + language_code: locale.language_code) } end def update_deerjikists @@ -404,7 +409,8 @@ class TagsController < ApplicationController return if performed? - render json: DeerjikistRepr.many(tag.reload.deerjikists) + render json: DeerjikistRepr.many(tag.reload.deerjikists, + language_code: locale.language_code) end def materials_by_name @@ -422,7 +428,7 @@ class TagsController < ApplicationController .find_by(tag_names: { name:, language_code: locale.language_code, primary_flg: true }) return head :not_found unless tag - graph = build_with_depth_graph(material_filter) + graph = build_with_depth_graph(material_filter, locale.language_code) render json: build_tag_children(tag, graph:, language_code: locale.language_code) end @@ -543,9 +549,7 @@ class TagsController < ApplicationController value end - def build_with_depth_graph material_filter - locale = resolve_locale! - + def build_with_depth_graph material_filter, language_code children_by_parent_id = Hash.new { |h, k| h[k] = [] } parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] } @@ -564,7 +568,7 @@ class TagsController < ApplicationController Tag .joins(:tag_names) .where(id: tag_ids, - tag_names: { language_code: locale.language_code, primary_flg: true }) + tag_names: { language_code:, primary_flg: true }) .pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at') .each_with_object({ }) do |(id, name, category, deprecated_at), h| h[id] = { name:, category:, deprecated: deprecated_at.present?, @@ -712,7 +716,10 @@ class TagsController < ApplicationController TagRepr.base(tag, language_code:).merge( children: children.map { build_tag_children(_1, graph:, language_code:) }, has_material: tag_graph ? tag_graph[:has_material] : material.present?, - material: material && MaterialRepr.base(material, host: request.base_url)) + material: material && MaterialRepr.base( + material, + host: request.base_url, + language_code:)) end def record_tag_version! tag, event_type:, created_by_user:, language_code: 'ja', diff --git a/backend/app/models/tag_version.rb b/backend/app/models/tag_version.rb index 6ca5d0a..8b6e31a 100644 --- a/backend/app/models/tag_version.rb +++ b/backend/app/models/tag_version.rb @@ -11,5 +11,6 @@ class TagVersion < ApplicationRecord meta: 'meta' }, validate: true validates :name, presence: true + validates :names, presence: true validates :category, presence: true end diff --git a/backend/app/representations/deerjikist_repr.rb b/backend/app/representations/deerjikist_repr.rb index 552e350..dbe8fb8 100644 --- a/backend/app/representations/deerjikist_repr.rb +++ b/backend/app/representations/deerjikist_repr.rb @@ -6,12 +6,12 @@ module DeerjikistRepr module_function - def base deerjikist + def base deerjikist, language_code: 'ja' deerjikist.as_json(BASE).merge( - tag: deerjikist.tag && TagRepr.base(deerjikist.tag, language_code: 'ja')) + tag: deerjikist.tag && TagRepr.base(deerjikist.tag, language_code:)) end - def many deerjikists - deerjikists.map { |d| base(d) } + def many deerjikists, language_code: 'ja' + deerjikists.map { |d| base(d, language_code:) } end end diff --git a/backend/app/representations/material_repr.rb b/backend/app/representations/material_repr.rb index 4893dbd..0482569 100644 --- a/backend/app/representations/material_repr.rb +++ b/backend/app/representations/material_repr.rb @@ -12,15 +12,15 @@ module MaterialRepr module_function - def base material, host: + def base material, host:, language_code: 'ja' material.as_json(BASE).merge( - tag: material.tag && TagRepr.base(material.tag, language_code: 'ja'), + tag: material.tag && TagRepr.base(material.tag, language_code:), file: if material.file.attached? Rails.application.routes.url_helpers.rails_storage_proxy_url( material.file, host:) end, thumbnail: thumbnail_url(material, host:), - thumbnail_fallback_text: thumbnail_fallback_text(material), + thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:), thumbnail_fallback_kind: thumbnail_fallback_kind(material), media_kind: media_kind(material), file_byte_size: material.file_byte_size, @@ -28,11 +28,11 @@ module MaterialRepr export_items: export_items(material)) end - def many materials, host: - materials.map { |m| base(m, host:) } + def many materials, host:, language_code: 'ja' + materials.map { |m| base(m, host:, language_code:) } end - def list material, host: + def list material, host:, language_code: 'ja' { id: material.id, version_no: material.version_no, url: material.url, @@ -41,9 +41,9 @@ module MaterialRepr source_path: material.source_path, source_file_id: material.source_file_id, normalized_source_key: material.normalized_source_key, - tag: compact_tag(material.tag), + tag: compact_tag(material.tag, language_code:), thumbnail: thumbnail_url(material, host:), - thumbnail_fallback_text: thumbnail_fallback_text(material), + thumbnail_fallback_text: thumbnail_fallback_text(material, language_code:), thumbnail_fallback_kind: thumbnail_fallback_kind(material), media_kind: media_kind(material), content_type: material.content_type, @@ -54,8 +54,8 @@ module MaterialRepr export_items: export_items(material) } end - def list_many materials, host: - materials.map { |m| list(m, host:) } + def list_many materials, host:, language_code: 'ja' + materials.map { |m| list(m, host:, language_code:) } end def export_paths material @@ -80,8 +80,8 @@ module MaterialRepr material.thumbnail, host:) end - def thumbnail_fallback_text material - material.tag&.name('ja') || material.created_at&.strftime('%Y-%m-%d') + def thumbnail_fallback_text material, language_code: + material.tag&.name(language_code) || material.created_at&.strftime('%Y-%m-%d') end def thumbnail_fallback_kind material @@ -99,11 +99,11 @@ module MaterialRepr 'file_other' end - def compact_tag tag + def compact_tag tag, language_code: return nil unless tag { id: tag.id, - name: tag.name('ja'), + name: tag.name(language_code), category: tag.category, deprecated_at: tag.deprecated_at } end diff --git a/backend/app/representations/post_repr.rb b/backend/app/representations/post_repr.rb index fce7ccf..abd0dd2 100644 --- a/backend/app/representations/post_repr.rb +++ b/backend/app/representations/post_repr.rb @@ -17,23 +17,23 @@ module PostRepr module_function - def base post, current_user = nil, host: nil + def base post, current_user = nil, host: nil, language_code: 'ja' json = if host.present? common(post, host:) else common(post) end - json['tags'] = tag_json(post) + json['tags'] = tag_json(post, language_code:) json['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user) json['viewed'] = current_user ? current_user.viewed?(post) : false json end def detail post, current_user = nil, parent_posts: [], child_posts: [], - sibling_posts: { }, related: [], host: nil + sibling_posts: { }, related: [], host: nil, language_code: 'ja' if host.present? - base(post, current_user, host:).merge( + base(post, current_user, host:, language_code:).merge( 'parent_posts' => cards(parent_posts, host:), 'child_posts' => cards(child_posts, host:), 'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts| @@ -41,7 +41,7 @@ module PostRepr }, 'related' => cards(related, host:)) else - base(post, current_user).merge( + base(post, current_user, language_code:).merge( 'parent_posts' => cards(parent_posts), 'child_posts' => cards(child_posts), 'sibling_posts' => sibling_posts.transform_keys(&:to_s).transform_values { |posts| @@ -86,14 +86,14 @@ module PostRepr end) end - def tag_json post + def tag_json post, language_code: internal_tags = post .post_tags .reject { _1.tag.deprecated? } - .sort_by { _1.tag.name('ja') } + .sort_by { _1.tag.name(language_code) } .map { |post_tag| - TagRepr.inline(post_tag.tag).merge( + TagRepr.inline(post_tag.tag, language_code:).merge( 'children' => [], 'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms])) } diff --git a/backend/app/services/post_create_preflight.rb b/backend/app/services/post_create_preflight.rb index 24eb21a..c27fc3c 100644 --- a/backend/app/services/post_create_preflight.rb +++ b/backend/app/services/post_create_preflight.rb @@ -18,6 +18,7 @@ class PostCreatePreflight def run locale preview = PostImportPreviewer.new.preview_rows( rows: [preview_row], + locale:, fetch_metadata: false).first if preview[:existing_post_id].present? return { diff --git a/backend/app/services/post_import_previewer.rb b/backend/app/services/post_import_previewer.rb index 6ce25e2..a81a6b6 100644 --- a/backend/app/services/post_import_previewer.rb +++ b/backend/app/services/post_import_previewer.rb @@ -16,7 +16,7 @@ class PostImportPreviewer THUMBNAIL_FETCH_WARNING = 'サムネールを取得できませんでした.'.freeze METADATA_FETCH_WARNING = '自動取得に失敗しました.'.freeze - def preview_rows locale, rows:, fetch_metadata: true, metadata_cache: { } + def preview_rows rows:, locale: Locale.nipponese, fetch_metadata: true, metadata_cache: { } prepared_rows = rows.map { prepare_row(_1) } url_counts = prepared_rows.filter_map { _1[:normal_url] }.tally existing_posts = @@ -30,6 +30,7 @@ class PostImportPreviewer url_counts) known_tags = preload_known_tags( + locale, prepared_rows, fetch_metadata, metadata_cache, @@ -347,7 +348,7 @@ class PostImportPreviewer { data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } } end - def preload_known_tags prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts + def preload_known_tags locale, prepared_rows, fetch_metadata, metadata_cache, existing_posts, url_counts names = prepared_rows.flat_map { |row| attributes = initial_attributes(row) provenance = initial_provenance(row) @@ -359,11 +360,11 @@ class PostImportPreviewer metadata = metadata_for(row[:normal_url], metadata_cache) apply_metadata!(attributes, provenance, tag_sources, metadata[:data]) end - preview_tag_names(merged_tags(tag_sources, provenance['tags'])) + preview_tag_names(locale, merged_tags(tag_sources, provenance['tags'])) }.compact.uniq return { } if names.empty? - TagName.where(language_code: 'ja', primary_flg: true, name: names) + TagName.where(language_code: locale.language_code, primary_flg: true, name: names) .includes(:tag) .each_with_object({ }) { |tag_name, tags| tags[tag_name.name] = tag_name.tag } end diff --git a/backend/app/services/tag_version_recorder.rb b/backend/app/services/tag_version_recorder.rb index c7c8bd4..d5debc8 100644 --- a/backend/app/services/tag_version_recorder.rb +++ b/backend/app/services/tag_version_recorder.rb @@ -1,11 +1,10 @@ class TagVersionRecorder < VersionRecorder - def self.record! tag:, event_type:, created_by_user:, language_code: 'ja' - new(tag:, event_type:, created_by_user:, language_code:).record! + def self.record! tag:, event_type:, created_by_user:, **_options + new(tag:, event_type:, created_by_user:).record! end - def initialize tag:, event_type:, created_by_user:, language_code: + def initialize tag:, event_type:, created_by_user: super(record: tag, event_type:, created_by_user:) - @language_code = language_code end private @@ -15,10 +14,24 @@ class TagVersionRecorder < VersionRecorder def record_key = :tag def snapshot_attributes - { name: @record.name(@language_code), + names = snapshot_names + japanese_names = names.select { _1[:language_code] == 'ja' } + compatibility_names = japanese_names.presence || names + + { name: compatibility_names.find { _1[:primary_flg] }[:name], category: @record.category, deprecated_at: @record.deprecated_at, - aliases: @record.snapshot_aliases(@language_code).join(' '), + aliases: compatibility_names.reject { _1[:primary_flg] }.map { _1[:name] }.join(' '), + names:, parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') } end + + def snapshot_names + @record.tag_names.order(:language_code, primary_flg: :desc).order(:name).map { |tag_name| + { language_code: tag_name.language_code, + name: tag_name.name, + script_code: tag_name.script_code, + primary_flg: tag_name.primary_flg } + } + end end diff --git a/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb b/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb new file mode 100644 index 0000000..4a84eb2 --- /dev/null +++ b/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb @@ -0,0 +1,36 @@ +class AddNamesToTagVersions < ActiveRecord::Migration[8.0] + class TagVersion < ActiveRecord::Base + self.table_name = 'tag_versions' + end + + def up + add_column :tag_versions, :names, :json + TagVersion.reset_column_information + + say_with_time 'Backfilling multilingual tag version names' do + TagVersion.find_each do |version| + names = [{ + language_code: 'ja', + name: version.name, + script_code: 'Jpan', + primary_flg: true }] + names.concat(version.aliases.to_s.split.filter_map { |name| + next if name.blank? + + { + language_code: 'ja', + name:, + script_code: 'Jpan', + primary_flg: false } + }) + version.update_columns(names: names) + end + end + + change_column_null :tag_versions, :names, false + end + + def down + remove_column :tag_versions, :names + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index c6b886b..8862e73 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_09_22_030000) do +ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -480,6 +480,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_030000) do t.datetime "deprecated_at" t.datetime "created_at", null: false t.bigint "created_by_user_id" + t.json "names", null: false t.index ["created_at"], name: "index_tag_versions_on_created_at" t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc } t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc } diff --git a/backend/spec/models/version_record_spec.rb b/backend/spec/models/version_record_spec.rb index 32a28ed..b5cfffc 100644 --- a/backend/spec/models/version_record_spec.rb +++ b/backend/spec/models/version_record_spec.rb @@ -12,6 +12,8 @@ RSpec.describe VersionRecord, type: :model do name: primary_tag_name_for(tag, 'ja').name, category: tag.category, aliases: '', + names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name, + script_code: 'Jpan', primary_flg: true }], parent_tag_ids: '', created_at: Time.current, created_by_user: nil @@ -30,6 +32,8 @@ RSpec.describe VersionRecord, type: :model do name: primary_tag_name_for(tag, 'ja').name, category: tag.category, aliases: '', + names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name, + script_code: 'Jpan', primary_flg: true }], parent_tag_ids: '', created_at: Time.current, created_by_user: nil diff --git a/backend/spec/requests/tag_versions_spec.rb b/backend/spec/requests/tag_versions_spec.rb index 4aa441b..3a6383a 100644 --- a/backend/spec/requests/tag_versions_spec.rb +++ b/backend/spec/requests/tag_versions_spec.rb @@ -36,6 +36,11 @@ RSpec.describe 'TagVersions API', type: :request do category: category, deprecated_at: deprecated_at, aliases: Array(aliases).join(' '), + names: [{ language_code: 'ja', name: name, script_code: 'Jpan', + primary_flg: true }] + Array(aliases).map { |alias_name| + { language_code: 'ja', name: alias_name, script_code: 'Jpan', + primary_flg: false } + }, parent_tag_ids: Array(parent_tags).map(&:id).join(' '), created_by_user: created_by_user, created_at: created_at) diff --git a/backend/spec/services/tag_version_recorder_spec.rb b/backend/spec/services/tag_version_recorder_spec.rb new file mode 100644 index 0000000..9bec5d7 --- /dev/null +++ b/backend/spec/services/tag_version_recorder_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +RSpec.describe TagVersionRecorder do + let(:user) { create_member_user! } + let(:tag) { create(:tag, primary_name: '日本語名') } + + before do + Language.insert_all!([{ code: 'en', name: 'English', created_at: Time.current }]) + Script.insert_all!([{ code: 'Latn', name: 'Latin', created_at: Time.current }]) + Locale.insert_all!([{ code: 'en', language_code: 'en', script_code: 'Latn', + name: 'English', created_at: Time.current }]) + TagName.create!(tag:, language_code: 'ja', script_code: 'Jpan', name: '別名', + primary_flg: false, + canonical_id: tag.tag_name_id) + TagName.create!(tag:, language_code: 'en', script_code: 'Latn', name: 'English name', + primary_flg: true) + TagName.create!(tag:, language_code: 'en', script_code: 'Latn', name: 'English alias', + primary_flg: false) + end + + it 'snapshots every language primary name and alias deterministically' do + version = described_class.record!(tag:, event_type: :create, created_by_user: user) + + expect(version.names).to eq([ + { 'language_code' => 'en', 'name' => 'English name', 'script_code' => 'Latn', + 'primary_flg' => true }, + { 'language_code' => 'en', 'name' => 'English alias', 'script_code' => 'Latn', + 'primary_flg' => false }, + { 'language_code' => 'ja', 'name' => '日本語名', 'script_code' => 'Jpan', + 'primary_flg' => true }, + { 'language_code' => 'ja', 'name' => '別名', 'script_code' => 'Jpan', + 'primary_flg' => false } + ]) + end + + it 'creates a version for an English-only name change and preserves prior names' do + original = described_class.record!(tag:, event_type: :create, created_by_user: user) + TagName.find_by!(tag:, language_code: 'en', primary_flg: true).update!(name: 'Renamed') + + updated = described_class.record!(tag:, event_type: :update, created_by_user: user) + + expect(updated.version_no).to eq(2) + expect(original.reload.names).to include( + hash_including('language_code' => 'en', 'name' => 'English name')) + expect(updated.names).to include(hash_including('language_code' => 'en', 'name' => 'Renamed')) + end +end