このコミットが含まれているのは:
@@ -1,6 +1,11 @@
|
||||
class ApplicationController < ActionController::API
|
||||
class UnsupportedLocale < StandardError
|
||||
;
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
rescue_from ActiveRecord::RecordNotUnique, with: :render_record_not_unique
|
||||
rescue_from(UnsupportedLocale) { head :not_found }
|
||||
|
||||
before_action :reject_banned_ip_address!
|
||||
before_action :authenticate_user
|
||||
@@ -93,5 +98,15 @@ class ApplicationController < ActionController::API
|
||||
value
|
||||
end
|
||||
|
||||
def resolve_locale! = Locale.find_by(code: params[:locale]) || Locale.nipponese
|
||||
def resolve_locale!
|
||||
code = params.fetch(:locale, 'ja')
|
||||
unless code.is_a?(String) && Locale.codes.value?(code)
|
||||
raise UnsupportedLocale
|
||||
end
|
||||
|
||||
locale = Locale.find_by(code:)
|
||||
raise UnsupportedLocale unless locale && locale.deprecated_at.nil?
|
||||
|
||||
locale
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Language < ApplicationRecord
|
||||
has_many :locales, foreign_key: :language_code, primary_key: :code
|
||||
|
||||
enum :code, ja: 'ja'
|
||||
enum :code, ja: 'ja', ain: 'ain'
|
||||
end
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
class Locale < ApplicationRecord
|
||||
after_create_commit :generate_tag_names!
|
||||
after_save :generate_tag_names!, if: :needs_tag_names?
|
||||
|
||||
belongs_to :language, foreign_key: :language_code, primary_key: :code
|
||||
belongs_to :script, foreign_key: :script_code, primary_key: :code
|
||||
|
||||
enum :code, ja: 'ja', en: 'en', fr: 'fr'
|
||||
enum :code, ja: 'ja', ain_kana: 'ain-Kana'
|
||||
|
||||
def self.nipponese = Locale.find('ja')
|
||||
|
||||
private
|
||||
|
||||
def needs_tag_names?
|
||||
deprecated_at.nil? && (saved_change_to_code? || saved_change_to_deprecated_at?)
|
||||
end
|
||||
|
||||
def generate_tag_names!
|
||||
ApplicationRecord.transaction do
|
||||
tag_ids = TagName.where(language_code:, primary_flg: true).pluck(:tag_id)
|
||||
Tag.where.not(id: tag_ids).includes(:tag_names).find_each do
|
||||
TagName.create!(tag_id: _1.id,
|
||||
language_code:,
|
||||
name: TagName.generate_name(self, _1, _1.tag_name('ja')),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
auto_generated: true)
|
||||
end
|
||||
end
|
||||
TagNameCompletion.for_locale!(self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Script < ApplicationRecord
|
||||
has_many :locales, foreign_key: :script_code, primary_key: :code
|
||||
|
||||
enum :code, Jpan: 'Jpan'
|
||||
enum :code, jpan: 'Jpan', kana: 'Kana'
|
||||
end
|
||||
|
||||
+14
-28
@@ -111,7 +111,12 @@ class Tag < ApplicationRecord
|
||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||
|
||||
def snapshot_aliases(language_code)
|
||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
||||
if tag_names.loaded?
|
||||
tag_names.select { _1.language_code == language_code && !(_1.primary?) }
|
||||
.sort_by(&:name).map(&:name)
|
||||
else
|
||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||
@@ -263,16 +268,19 @@ class Tag < ApplicationRecord
|
||||
next tn.tag if tn
|
||||
|
||||
tag = Tag.create!(category:)
|
||||
tn = TagName.create!(tag:, language_code:, name:,
|
||||
script_code: locale.script_code, primary_flg: true,
|
||||
auto_generated: false)
|
||||
generate_tag_names!(locale, tag, tn)
|
||||
TagName.create!(tag:, language_code:, name:,
|
||||
script_code: locale.script_code, primary_flg: true,
|
||||
auto_generated: false)
|
||||
TagNameCompletion.for_tag!(tag)
|
||||
tag.association(:tag_names).reset
|
||||
|
||||
tag
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
existing = TagName.find_by(language_code:, name:)
|
||||
raise unless existing
|
||||
|
||||
existing.tag
|
||||
end
|
||||
|
||||
def merge_tags! target_tag, source_tags, created_by_user: nil
|
||||
@@ -352,28 +360,6 @@ class Tag < ApplicationRecord
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_tag_names! locale, tag, base_tag_name
|
||||
ApplicationRecord.transaction do
|
||||
locales =
|
||||
Locale
|
||||
.where(deprecated_at: nil)
|
||||
.where.not(language_code: locale.language_code)
|
||||
.order(:language_code, :created_at, :script_code)
|
||||
.uniq { _1.language_code }
|
||||
locales.each do |locale|
|
||||
language_code = locale.language_code
|
||||
script_code = locale.script_code
|
||||
TagName.create!(tag_id: tag.id,
|
||||
language_code:,
|
||||
name: TagName.generate_name(locale, tag, base_tag_name),
|
||||
script_code:,
|
||||
primary_flg: true,
|
||||
auto_generated: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -34,9 +34,15 @@ class TagName < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.generate_name locale, tag, base_tag_name
|
||||
# TODO: 言語ごとの自動命名ロジック完成したら書く.
|
||||
fallback = "Tag_##{ tag.id }"
|
||||
return fallback unless locale.language_code == 'ain' && base_tag_name&.language_code == 'ja'
|
||||
|
||||
"Tag_##{ tag.id }"
|
||||
name = TagNameTranscription.ainu(base_tag_name.name)
|
||||
candidate = new(tag:, name:, language_code: 'ain', script_code: 'Kana',
|
||||
primary_flg: true, auto_generated: true)
|
||||
candidate.valid? ? name : fallback
|
||||
rescue TagNameTranscription::Unconvertible
|
||||
fallback
|
||||
end
|
||||
|
||||
private
|
||||
@@ -54,6 +60,8 @@ class TagName < ApplicationRecord
|
||||
end
|
||||
|
||||
def name_must_be_sanitised
|
||||
return if auto_generated && name == "Tag_##{ tag_id }"
|
||||
|
||||
if name? && name != TagNameSanitisationRule.sanitise(name)
|
||||
errors.add :name, '名前に使用できない文字が含まれてゐます.'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
class TagNameCompletion
|
||||
def self.for_locale! locale
|
||||
named_tags = TagName.where(language_code: locale.language_code, primary_flg: true)
|
||||
.select(:tag_id)
|
||||
Tag.where.not(id: named_tags).find_each do |tag|
|
||||
for_tag!(tag, locales: [locale])
|
||||
end
|
||||
end
|
||||
|
||||
def self.for_tag! tag, locales: nil
|
||||
locales ||= Locale.where(deprecated_at: nil).order(:language_code, :created_at, :script_code)
|
||||
tag.with_lock do
|
||||
primary_names = tag.tag_names.where(primary_flg: true).index_by(&:language_code)
|
||||
locales.to_a.uniq(&:language_code).each do |locale|
|
||||
next if primary_names.key?(locale.language_code)
|
||||
|
||||
name = TagName.generate_name(locale, tag, primary_names['ja'])
|
||||
primary_names[locale.language_code] = create_name!(locale, tag, name)
|
||||
end
|
||||
tag.association(:tag_names).reset
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_name! locale, tag, name
|
||||
persist_name!(locale, tag, name)
|
||||
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => error
|
||||
fallback = "Tag_##{ tag.id }"
|
||||
collision = error.is_a?(ActiveRecord::RecordNotUnique) ||
|
||||
error.record.errors.of_kind?(:name, :taken)
|
||||
raise unless name != fallback && collision
|
||||
|
||||
persist_name!(locale, tag, fallback)
|
||||
end
|
||||
private_class_method :create_name!
|
||||
|
||||
def self.persist_name! locale, 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)
|
||||
end
|
||||
end
|
||||
private_class_method :persist_name!
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
module TagNameTranscription
|
||||
class Unconvertible < StandardError
|
||||
;
|
||||
end
|
||||
|
||||
def self.ainu name
|
||||
previous_script = nil
|
||||
JapaneseSegments.split(name).map { |script, text|
|
||||
boundary = previous_script && script != previous_script &&
|
||||
JapaneseSegments.japanese?(previous_script) && JapaneseSegments.japanese?(script)
|
||||
previous_script = script
|
||||
reading = script == :kanji ? JisReadings.read(text) : text
|
||||
output =
|
||||
if script == :literal
|
||||
text
|
||||
else
|
||||
Katakana.render(Phonology.adapt(reading))
|
||||
end
|
||||
|
||||
boundary ? "_#{ output }" : output
|
||||
}.join
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
module TagNameTranscription
|
||||
module JapaneseSegments
|
||||
JAPANESE_SCRIPTS = [:kanji, :hiragana, :katakana].freeze
|
||||
|
||||
def self.japanese?(script) = JAPANESE_SCRIPTS.include?(script)
|
||||
|
||||
def self.split name
|
||||
segments = []
|
||||
name.to_s.scan(/\X/).each do |character|
|
||||
script = classify(character, segments.last&.first)
|
||||
if segments.last&.first == script
|
||||
segments.last[1] << character
|
||||
else
|
||||
segments << [script, character.dup]
|
||||
end
|
||||
end
|
||||
segments
|
||||
end
|
||||
|
||||
def self.classify character, previous_script
|
||||
return previous_script if character == 'ー' && japanese?(previous_script)
|
||||
return :kanji if character.match?(/\A\p{Han}\z/)
|
||||
return :hiragana if character.match?(/\A\p{Hiragana}\p{M}*\z/)
|
||||
return :katakana if character.match?(/\A\p{Katakana}\p{M}*\z/)
|
||||
return :literal if character.match?(/\A(?:\p{Latin}\p{M}*|[!-~])\z/)
|
||||
|
||||
raise Unconvertible
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
module TagNameTranscription
|
||||
module JisReadings
|
||||
SOURCE = Rails.root.join('config/tag_name_readings/jis_level_one.tsv')
|
||||
READINGS = File.foreach(SOURCE, chomp: true).reject { _1.start_with?('#') }.to_h { |line|
|
||||
character, reading, = line.split("\t")
|
||||
[character, reading]
|
||||
}.freeze
|
||||
|
||||
def self.read text
|
||||
text.each_char.map { READINGS.fetch(_1) { raise Unconvertible } }.join
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
module TagNameTranscription
|
||||
module Katakana
|
||||
ROWS = {
|
||||
'' => 'アイウエオ',
|
||||
'k' => 'カキクケコ',
|
||||
's' => 'サシスセソ',
|
||||
'c' => ['チャ', 'チ', 'チュ', 'チェ', 'チョ'],
|
||||
't' => ['タ', 'ティ', 'トゥ', 'テ', 'ト'],
|
||||
'n' => 'ナニヌネノ',
|
||||
'h' => 'ハヒフヘホ',
|
||||
'p' => 'パピプペポ',
|
||||
'm' => 'マミムメモ',
|
||||
'r' => 'ラリルレロ' }.freeze
|
||||
SYLLABLES = ROWS.flat_map { |onset, letters|
|
||||
kana = letters.is_a?(String) ? letters.chars : letters
|
||||
['a', 'i', 'u', 'e', 'o'].zip(kana).map { |vowel, letter| [onset + vowel, letter] }
|
||||
}.to_h.merge(
|
||||
'ya' => 'ヤ', 'yu' => 'ユ', 'yo' => 'ヨ', 'wa' => 'ワ',
|
||||
'k' => 'ㇰ', 's' => 'ㇱ', 't' => 'ッ', 'n' => 'ン', 'm' => 'ㇺ', 'p' => 'ㇷ゚',
|
||||
'r:a' => 'ㇻ', 'r:i' => 'ㇼ', 'r:u' => 'ㇽ', 'r:e' => 'ㇾ', 'r:o' => 'ㇿ').freeze
|
||||
|
||||
def self.render(sounds) = sounds.map { SYLLABLES.fetch(_1) }.join
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
module TagNameTranscription
|
||||
module Phonology
|
||||
ROWS = {
|
||||
'アイウエオ' => ['a', 'i', 'u', 'e', 'o'],
|
||||
'カキクケコ' => ['ka', 'ki', 'ku', 'ke', 'ko'],
|
||||
'ガギグゲゴ' => ['ka', 'ki', 'ku', 'ke', 'ko'],
|
||||
'サシスセソ' => ['sa', 'si', 'su', 'se', 'so'],
|
||||
'ザジズゼゾ' => ['ca', 'ci', 'cu', 'ce', 'co'],
|
||||
'タチツテト' => ['ta', 'ci', 'cu', 'te', 'to'],
|
||||
'ダヂヅデド' => ['ta', 'ci', 'cu', 'te', 'to'],
|
||||
'ナニヌネノ' => ['na', 'ni', 'nu', 'ne', 'no'],
|
||||
'ハヒフヘホ' => ['ha', 'hi', 'hu', 'he', 'ho'],
|
||||
'バビブベボ' => ['pa', 'pi', 'pu', 'pe', 'po'],
|
||||
'パピプペポ' => ['pa', 'pi', 'pu', 'pe', 'po'],
|
||||
'マミムメモ' => ['ma', 'mi', 'mu', 'me', 'mo'],
|
||||
'ヤユヨ' => ['ya', 'yu', 'yo'],
|
||||
'ラリルレロ' => ['ra', 'ri', 'ru', 're', 'ro'],
|
||||
'ワ' => ['wa'],
|
||||
'ン' => ['n'],
|
||||
'ッ' => ['t'],
|
||||
'ㇰ' => ['k'],
|
||||
'ㇱㇲ' => ['s', 's'],
|
||||
'ㇳ' => ['t'],
|
||||
'ㇴ' => ['n'],
|
||||
'ㇺ' => ['m'],
|
||||
'ㇻㇼㇽㇾㇿ' => ['r:a', 'r:i', 'r:u', 'r:e', 'r:o'] }.freeze
|
||||
SOUNDS = ROWS.flat_map { |characters, sounds| characters.chars.zip(sounds) }.to_h.freeze
|
||||
|
||||
def self.adapt reading
|
||||
kana = reading.unicode_normalize(:nfc).tr('ぁ-ゖ', 'ァ-ヶ').tr('ャュョ', 'ヤユヨ')
|
||||
kana.delete('ー').scan(/\X/).map { |character|
|
||||
character == 'ㇷ゚' ? 'p' : SOUNDS.fetch(character) { raise Unconvertible }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
新しいイシューから参照
ユーザーをブロックする