このコミットが含まれているのは:
@@ -10,6 +10,7 @@ class ApplicationController < ActionController::API
|
||||
before_action :reject_banned_ip_address!
|
||||
before_action :authenticate_user
|
||||
before_action :reject_banned_user!
|
||||
before_action :validate_explicit_locale!
|
||||
|
||||
def current_user = @current_user
|
||||
|
||||
@@ -99,6 +100,8 @@ class ApplicationController < ActionController::API
|
||||
end
|
||||
|
||||
def resolve_locale!
|
||||
return @resolved_locale if defined?(@resolved_locale)
|
||||
|
||||
code = params.fetch(:locale, 'ja')
|
||||
unless code.is_a?(String) && Locale.codes.value?(code)
|
||||
raise UnsupportedLocale
|
||||
@@ -107,6 +110,10 @@ class ApplicationController < ActionController::API
|
||||
locale = Locale.find_by(code:)
|
||||
raise UnsupportedLocale unless locale && locale.deprecated_at.nil?
|
||||
|
||||
locale
|
||||
@resolved_locale = locale
|
||||
end
|
||||
|
||||
def validate_explicit_locale!
|
||||
resolve_locale! if params.key?(:locale)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
class LocalesController < ApplicationController
|
||||
def index
|
||||
locales = Locale.where(deprecated_at: nil).order(:created_at).map do |locale|
|
||||
{ code: locale.code_before_type_cast, name: locale.name }
|
||||
end
|
||||
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
render json: { locales: }
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,5 @@ class Locale < ApplicationRecord
|
||||
deprecated_at.nil? && (saved_change_to_code? || saved_change_to_deprecated_at?)
|
||||
end
|
||||
|
||||
def generate_tag_names!
|
||||
TagNameCompletion.for_locale!(self)
|
||||
end
|
||||
def generate_tag_names! = TagNameCompletion.for_locale!(self)
|
||||
end
|
||||
|
||||
@@ -110,13 +110,8 @@ class Tag < ApplicationRecord
|
||||
|
||||
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
|
||||
|
||||
def snapshot_aliases(language_code)
|
||||
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
|
||||
def snapshot_aliases language_code
|
||||
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
|
||||
end
|
||||
|
||||
def snapshot_parent_tag_ids = parents.order(:id).pluck(:id)
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
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)
|
||||
class << self
|
||||
def 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 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
|
||||
|
||||
private
|
||||
|
||||
def 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 if name == fallback || !(collision)
|
||||
|
||||
persist_name!(locale, tag, fallback)
|
||||
end
|
||||
|
||||
def 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
|
||||
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
|
||||
|
||||
@@ -5,19 +5,52 @@ module TagNameTranscription
|
||||
|
||||
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
|
||||
parts = []
|
||||
NipponeseSegments.split(name).each do |script, text|
|
||||
output = transcribe_segment(script, text)
|
||||
if (parts.last && output.first && previous_script && script != previous_script &&
|
||||
NipponeseSegments.nipponese?(previous_script) && NipponeseSegments.nipponese?(script))
|
||||
parts << '_'
|
||||
end
|
||||
|
||||
boundary ? "_#{ output }" : output
|
||||
parts.concat(output)
|
||||
previous_script = script
|
||||
end
|
||||
|
||||
replace_unconvertible(parts)
|
||||
end
|
||||
|
||||
def self.transcribe_segment script, text
|
||||
return [text] if script == :literal
|
||||
return [nil] if script == :unconvertible
|
||||
return transcribe_reading(text) unless script == :kanzi
|
||||
|
||||
text.each_char.flat_map do |character|
|
||||
transcribe_reading(JisReadings.read(character))
|
||||
rescue Unconvertible
|
||||
[nil]
|
||||
end
|
||||
end
|
||||
|
||||
def self.transcribe_reading reading
|
||||
Phonology.adapt(reading).map do |sound|
|
||||
sound && Katakana.render([sound])
|
||||
rescue Unconvertible
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def self.replace_unconvertible parts
|
||||
grouped = parts.chunk { _1.nil? }.flat_map { |unconvertible, group|
|
||||
unconvertible ? [nil] : group
|
||||
}
|
||||
first = grouped.index { _1&.match?(/[^_]/) }
|
||||
last = grouped.rindex { _1&.match?(/[^_]/) }
|
||||
|
||||
grouped.each_with_index.map { |part, index|
|
||||
part || (first && first < index && index < last ? '_' : '')
|
||||
}.join
|
||||
end
|
||||
|
||||
private_class_method :transcribe_segment, :transcribe_reading, :replace_unconvertible
|
||||
end
|
||||
|
||||
@@ -19,6 +19,8 @@ module TagNameTranscription
|
||||
'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
|
||||
def self.render sounds
|
||||
sounds.map { SYLLABLES.fetch(_1) { raise Unconvertible } }.join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+7
-7
@@ -1,8 +1,8 @@
|
||||
module TagNameTranscription
|
||||
module JapaneseSegments
|
||||
JAPANESE_SCRIPTS = [:kanji, :hiragana, :katakana].freeze
|
||||
module NipponeseSegments
|
||||
NIPPONESE_SCRIPTS = [:kanzi, :xiragana, :katakana].freeze
|
||||
|
||||
def self.japanese?(script) = JAPANESE_SCRIPTS.include?(script)
|
||||
def self.nipponese?(script) = NIPPONESE_SCRIPTS.include?(script)
|
||||
|
||||
def self.split name
|
||||
segments = []
|
||||
@@ -18,13 +18,13 @@ module TagNameTranscription
|
||||
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 previous_script if character == 'ー' && nipponese?(previous_script)
|
||||
return :kanzi if character.match?(/\A\p{Han}\z/)
|
||||
return :xiragana 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
|
||||
:unconvertible
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -24,13 +24,36 @@ module TagNameTranscription
|
||||
'ㇴ' => ['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
|
||||
|
||||
DIGRAPHS = {
|
||||
'シャ' => 'sa', 'シュ' => 'su', 'シェ' => 'se', 'ショ' => 'so',
|
||||
'チャ' => 'ca', 'チュ' => 'cu', 'チェ' => 'ce', 'チョ' => 'co',
|
||||
'ジャ' => 'ca', 'ジュ' => 'cu', 'ジェ' => 'ce', 'ジョ' => 'co',
|
||||
'ヂャ' => 'ca', 'ヂュ' => 'cu', 'ヂェ' => 'ce', 'ヂョ' => 'co' }.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 }
|
||||
}
|
||||
kana = reading.unicode_normalize(:nfc).tr('ぁ-ゖ', 'ァ-ヶ').delete('ー')
|
||||
characters = kana.scan(/\X/)
|
||||
sounds = []
|
||||
index = 0
|
||||
|
||||
while index < characters.length
|
||||
pair = characters[index, 2].join
|
||||
|
||||
if DIGRAPHS.key?(pair)
|
||||
sounds << DIGRAPHS.fetch(pair)
|
||||
index += 2
|
||||
next
|
||||
end
|
||||
|
||||
character = characters[index].tr('ャュョ', 'ヤユヨ')
|
||||
sounds << (character == 'ㇷ゚' ? 'p' : SOUNDS[character])
|
||||
index += 1
|
||||
end
|
||||
|
||||
sounds
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Rails.application.routes.draw do
|
||||
resources :locales, only: [:index]
|
||||
|
||||
resources :tag_groups, only: [:index, :show, :create, :update]
|
||||
|
||||
resources :nico_tags, path: 'tags/nico', only: [:index, :show, :update]
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
ApplicationRecord.transaction do
|
||||
Language.find_or_create_by!(code: 'ain') { _1.name = 'ain' }
|
||||
Script.find_or_create_by!(code: 'Kana') { _1.name = 'Kana' }
|
||||
locale = Locale.find_or_initialize_by(code: 'ain-Kana')
|
||||
locale.assign_attributes(language_code: 'ain', script_code: 'Kana', region_code: 392,
|
||||
variants: '', name: 'ain-Kana', deprecated_at: nil)
|
||||
locale.save!
|
||||
TagNameCompletion.for_locale!(locale)
|
||||
end
|
||||
|
||||
post_url_sanitisation_rules = [
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする