このコミットが含まれているのは:
2026-10-18 15:36:09 +09:00
コミット 20085d5017
3個のファイルの変更、151行の追加、11行の削除
+3 -3
ファイルの表示
@@ -1,4 +1,6 @@
class Locale < ApplicationRecord class Locale < ApplicationRecord
attr_accessor :certified_tag_names_path
before_save :lock_tag_creation!, if: :will_generate_tag_names? before_save :lock_tag_creation!, if: :will_generate_tag_names?
after_save :generate_tag_names!, if: :needs_tag_names? after_save :generate_tag_names!, if: :needs_tag_names?
@@ -15,9 +17,7 @@ class Locale < ApplicationRecord
deprecated_at.nil? && (will_save_change_to_code? || will_save_change_to_deprecated_at?) deprecated_at.nil? && (will_save_change_to_code? || will_save_change_to_deprecated_at?)
end end
def lock_tag_creation! def lock_tag_creation! = SharedResourceIdAllocator.lock_space!(Tag, 'tags')
SharedResourceIdAllocator.lock_space!(Tag, 'tags')
end
def needs_tag_names? def needs_tag_names?
deprecated_at.nil? && (saved_change_to_code? || saved_change_to_deprecated_at?) deprecated_at.nil? && (saved_change_to_code? || saved_change_to_deprecated_at?)
+103
ファイルの表示
@@ -0,0 +1,103 @@
require 'json'
require 'pathname'
class CertifiedTagNameTranslations
FORMAT = 'btrc-hub.certified-tag-name-translations.v1'.freeze
class Error < ArgumentError
;
end
def self.load! path, locale:
new(path, locale:).load!
end
def initialize path, locale:
@path = path
@locale = locale
end
def load!
document = read_document
unless document.is_a?(Hash) && document['format'] == FORMAT
fail!('invalid format or version')
end
source_code = document['source_locale']
target_code = document['target_locale']
unless target_code.is_a?(String) && target_code == Locale.codes.fetch(@locale.code)
fail!("target_locale #{ target_code.inspect } does not match the new Locale")
end
unless source_code.is_a?(String) && Locale.codes.value?(source_code)
fail!("invalid source_locale #{ source_code.inspect }")
end
source_locale = Locale.find_by(code: source_code)
fail!("source Locale #{ source_code.inspect } does not exist") unless source_locale
fail!('source_locale and target_locale must differ') if source_code == target_code
entries = validate_entries(document['entries'], source_code, target_code)
resolve_tags(entries, source_locale.language_code)
end
private
def read_document
unless (@path.is_a?(String) && !@path.include?("\0") &&
Pathname.new(@path).absolute?)
fail!('certified_tag_names_path must be an absolute file path')
end
fail!('JSON file does not exist or is not a regular file') unless File.file?(@path)
text = File.read(@path, encoding: Encoding::UTF_8)
fail!('JSON file is not valid UTF-8') unless text.valid_encoding?
JSON.parse(text)
rescue JSON::ParserError, SystemCallError, IOError, EncodingError => error
fail!("cannot read JSON: #{ error.message }")
end
def validate_entries entries, source_code, target_code
fail!('entries must be an array') unless entries.is_a?(Array)
sources = {}
targets = {}
entries.each_with_index.map do |entry, index|
fail!("entry #{ index + 1 } must be an object") unless entry.is_a?(Hash)
source = entry[source_code]
target = entry[target_code]
unless source.is_a?(String) && source.present? && target.is_a?(String) && target.present?
fail!("entry #{ index + 1 }: missing or empty #{ source_code }/#{ target_code } name")
end
fail!("duplicate source name #{ source.inspect }") if sources.key?(source)
fail!("duplicate target name #{ target.inspect }") if targets.key?(target)
sources[source] = true
targets[target] = true
[source, target]
end
end
def resolve_tags entries, source_language
source_names = entries.map(&:first)
# SQL may use a case/accent-insensitive collation; the Ruby keys are exact.
matches = TagName.where(language_code: source_language, primary_flg: true,
name: source_names).pluck(:name, :tag_id).group_by(&:first)
translations = {}
entries.each do |source, target|
rows = matches.fetch(source, [])
unless rows.length == 1
fail!("source primary name #{ source.inspect } must match exactly one tag")
end
tag_id = rows.first.last
if translations.key?(tag_id)
fail!("multiple source primary names resolve to Tag##{ tag_id }: #{ source.inspect }")
end
translations[tag_id] = target
end
translations
end
def fail! message
raise Error, "Certified tag names #{@path.inspect}: #{ message }"
end
end
+45 -8
ファイルの表示
@@ -3,6 +3,16 @@ class TagNameCompletion
def for_locale! locale def for_locale! locale
completed = Tag.transaction do completed = Tag.transaction do
SharedResourceIdAllocator.lock_space!(Tag, 'tags') SharedResourceIdAllocator.lock_space!(Tag, 'tags')
certified_names =
if locale.certified_tag_names_path.nil?
{ }
else
CertifiedTagNameTranslations.load!(locale.certified_tag_names_path, locale:)
end
Tag.where(id: certified_names.keys).lock.find_each(order: :asc) do |tag|
for_tag!(tag, locales: [locale],
certified_names: { locale.language_code => certified_names.fetch(tag.id) })
end
named_tags = named_tags =
TagName.where(language_code: locale.language_code, primary_flg: true) TagName.where(language_code: locale.language_code, primary_flg: true)
.select(:tag_id) .select(:tag_id)
@@ -16,7 +26,7 @@ class TagNameCompletion
end end
end end
def for_tag! tag, locales: nil def for_tag! tag, locales: nil, certified_names: { }
locales ||= locales ||=
Locale.where(deprecated_at: nil) Locale.where(deprecated_at: nil)
.order(:language_code, :created_at, :script_code).lock .order(:language_code, :created_at, :script_code).lock
@@ -24,6 +34,15 @@ class TagNameCompletion
SharedResourceIdAllocator.lock_space!(Tag, 'tags') SharedResourceIdAllocator.lock_space!(Tag, 'tags')
completed = tag.with_lock do completed = tag.with_lock do
primary_names = tag.tag_names.where(primary_flg: true).lock.index_by(&:language_code) primary_names = tag.tag_names.where(primary_flg: true).lock.index_by(&:language_code)
certified_names.each do |language, name|
primary = primary_names[language]
next unless primary
next if primary.name == name && !primary.auto_generated
raise CertifiedTagNameTranslations::Error,
"Tag##{ tag.id } already has a primary #{ language } name; " +
"cannot register certified name #{ name.inspect }"
end
missing_locales = locales.to_a.uniq(&:language_code).reject { |locale| missing_locales = locales.to_a.uniq(&:language_code).reject { |locale|
primary_names.key?(locale.language_code) primary_names.key?(locale.language_code)
} }
@@ -31,9 +50,14 @@ class TagNameCompletion
TagVersioning.ensure_snapshot!(tag, created_by_user: nil) TagVersioning.ensure_snapshot!(tag, created_by_user: nil)
missing_locales.each do |locale| missing_locales.each do |locale|
source_language = locale.language_code == 'ja' ? 'ain' : 'ja' primary_names[locale.language_code] =
name = TagName.generate_name(locale, tag, primary_names[source_language]) if certified_names.key?(locale.language_code)
primary_names[locale.language_code] = create_name!(locale, tag, name) create_certified_name!(locale, tag, certified_names.fetch(locale.language_code))
else
source_language = locale.language_code == 'ja' ? 'ain' : 'ja'
name = TagName.generate_name(locale, tag, primary_names[source_language])
create_name!(locale, tag, name)
end
end end
tag.association(:tag_names).reset tag.association(:tag_names).reset
version = TagVersioning.record!(tag, event_type: :update, created_by_user: nil) version = TagVersioning.record!(tag, event_type: :update, created_by_user: nil)
@@ -50,8 +74,21 @@ class TagNameCompletion
private private
def create_certified_name! locale, tag, name
persist_name!(locale, tag, name, auto_generated: false)
rescue ActiveRecord::RecordInvalid => error
raise unless error.record.is_a?(TagName)
raise CertifiedTagNameTranslations::Error,
"Tag##{ tag.id } certified name #{ name.inspect }: " +
error.record.errors.full_messages.join(', ')
rescue ActiveRecord::RecordNotUnique => error
raise CertifiedTagNameTranslations::Error,
"Tag##{ tag.id } certified name #{ name.inspect } conflicts: #{ error.message }"
end
def create_name! locale, tag, name def create_name! locale, tag, name
persist_name!(locale, tag, name) persist_name!(locale, tag, name, auto_generated: true)
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => error rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => error
fallback = "Tag_##{ tag.id }" fallback = "Tag_##{ tag.id }"
invalid_name = invalid_name =
@@ -63,13 +100,13 @@ class TagNameCompletion
end end
raise if name == fallback || !(invalid_name) raise if name == fallback || !(invalid_name)
persist_name!(locale, tag, fallback) persist_name!(locale, tag, fallback, auto_generated: true)
end end
def persist_name! locale, tag, name def persist_name! locale, tag, name, auto_generated:
tag_name = TagName.transaction(requires_new: true) do tag_name = TagName.transaction(requires_new: true) do
TagName.create!(tag:, name:, language_code: locale.language_code, TagName.create!(tag:, name:, language_code: locale.language_code,
script_code: locale.script_code, primary_flg: true, auto_generated: true) script_code: locale.script_code, primary_flg: true, auto_generated:)
end end
unless tag_name&.persisted? unless tag_name&.persisted?
raise ActiveRecord::RecordNotSaved, 'Generated tag name was not persisted' raise ActiveRecord::RecordNotSaved, 'Generated tag name was not persisted'