このコミットが含まれているのは:
2026-10-17 16:47:08 +09:00
コミット b945ceef60
91個のファイルの変更、4652行の追加、822行の削除
+16 -1
ファイルの表示
@@ -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 -1
ファイルの表示
@@ -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
+7 -13
ファイルの表示
@@ -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 -1
ファイルの表示
@@ -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
+10 -2
ファイルの表示
@@ -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
+43
ファイルの表示
@@ -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
+23
ファイルの表示
@@ -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
+30
ファイルの表示
@@ -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
+13
ファイルの表示
@@ -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
+24
ファイルの表示
@@ -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
+36
ファイルの表示
@@ -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