このコミットが含まれているのは:
@@ -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?)
|
||||||
|
|||||||
@@ -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
|
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'
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする