このコミットが含まれているのは:
@@ -1,4 +1,6 @@
|
||||
class Locale < ApplicationRecord
|
||||
attr_accessor :certified_tag_names_path
|
||||
|
||||
before_save :lock_tag_creation!, if: :will_generate_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?)
|
||||
end
|
||||
|
||||
def lock_tag_creation!
|
||||
SharedResourceIdAllocator.lock_space!(Tag, 'tags')
|
||||
end
|
||||
def lock_tag_creation! = SharedResourceIdAllocator.lock_space!(Tag, 'tags')
|
||||
|
||||
def needs_tag_names?
|
||||
deprecated_at.nil? && (saved_change_to_code? || saved_change_to_deprecated_at?)
|
||||
|
||||
@@ -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
|
||||
@@ -3,6 +3,16 @@ class TagNameCompletion
|
||||
def for_locale! locale
|
||||
completed = Tag.transaction do
|
||||
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 =
|
||||
TagName.where(language_code: locale.language_code, primary_flg: true)
|
||||
.select(:tag_id)
|
||||
@@ -16,7 +26,7 @@ class TagNameCompletion
|
||||
end
|
||||
end
|
||||
|
||||
def for_tag! tag, locales: nil
|
||||
def for_tag! tag, locales: nil, certified_names: { }
|
||||
locales ||=
|
||||
Locale.where(deprecated_at: nil)
|
||||
.order(:language_code, :created_at, :script_code).lock
|
||||
@@ -24,6 +34,15 @@ class TagNameCompletion
|
||||
SharedResourceIdAllocator.lock_space!(Tag, 'tags')
|
||||
completed = tag.with_lock do
|
||||
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|
|
||||
primary_names.key?(locale.language_code)
|
||||
}
|
||||
@@ -31,9 +50,14 @@ class TagNameCompletion
|
||||
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: nil)
|
||||
missing_locales.each do |locale|
|
||||
source_language = locale.language_code == 'ja' ? 'ain' : 'ja'
|
||||
name = TagName.generate_name(locale, tag, primary_names[source_language])
|
||||
primary_names[locale.language_code] = create_name!(locale, tag, name)
|
||||
primary_names[locale.language_code] =
|
||||
if certified_names.key?(locale.language_code)
|
||||
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
|
||||
tag.association(:tag_names).reset
|
||||
version = TagVersioning.record!(tag, event_type: :update, created_by_user: nil)
|
||||
@@ -50,8 +74,21 @@ class TagNameCompletion
|
||||
|
||||
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
|
||||
persist_name!(locale, tag, name)
|
||||
persist_name!(locale, tag, name, auto_generated: true)
|
||||
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => error
|
||||
fallback = "Tag_##{ tag.id }"
|
||||
invalid_name =
|
||||
@@ -63,13 +100,13 @@ class TagNameCompletion
|
||||
end
|
||||
raise if name == fallback || !(invalid_name)
|
||||
|
||||
persist_name!(locale, tag, fallback)
|
||||
persist_name!(locale, tag, fallback, auto_generated: true)
|
||||
end
|
||||
|
||||
def persist_name! locale, tag, name
|
||||
def persist_name! locale, tag, name, auto_generated:
|
||||
tag_name = TagName.transaction(requires_new: true) do
|
||||
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
|
||||
unless tag_name&.persisted?
|
||||
raise ActiveRecord::RecordNotSaved, 'Generated tag name was not persisted'
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする