このコミットが含まれているのは:
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
ファイル差分が大きすぎるため省略します 差分を読み込み
+10
ファイルの表示
@@ -8,6 +8,16 @@
# MovieGenre.find_or_create_by!(name: genre_name)
# end
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 = [
{ priority: 10,
source_pattern: '\Ahttps?://youtu\.be/([^/?#]+)(?:[?#].*)?\z',
+1 -11
ファイルの表示
@@ -24,17 +24,7 @@ FactoryBot.define do
evaluator.primary_tag_name.update!(tag:, primary_flg: true)
Locale.where(deprecated_at: nil).order(:language_code, :created_at, :script_code).to_a
.uniq(&:language_code).each do |locale|
next if tag.tag_names.exists?(language_code: locale.language_code,
primary_flg: true)
create(:tag_name,
tag:, language_code: locale.language_code,
script_code: locale.script_code,
name: TagName.generate_name(locale, tag, evaluator.primary_tag_name),
primary_flg: true, auto_generated: true)
end
TagNameCompletion.for_tag!(tag)
end
end
end
+2 -3
ファイルの表示
@@ -6,9 +6,8 @@ RSpec.describe Language, type: :model do
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
end
it 'accepts English when multilingual support is enabled' do
pending '日本語以外の Language 有効化は別課題で実装する'
expect { described_class.create!(code: 'en', name: 'English') }
it 'accepts Ainu when multilingual support is enabled' do
expect { described_class.create!(code: 'ain', name: 'ain') }
.to change(described_class, :count).by(1)
end
+82
ファイルの表示
@@ -0,0 +1,82 @@
require 'rails_helper'
RSpec.describe 'Locale activation', type: :model do
let!(:language) { Language.create!(code: 'ain', name: 'ain') }
let!(:script) { Script.create!(code: 'Kana', name: 'Kana') }
let!(:first) { create(:tag, primary_name: 'activation_first') }
let!(:second) { create(:tag, primary_name: 'activation_second') }
def build_locale deprecated_at: nil
Locale.new(code: 'ain-Kana', language_code: language.code,
script_code: script.code, region_code: 392, variants: '',
name: 'ain-Kana', deprecated_at:)
end
def generated_names
TagName.where(language_code: 'ain', primary_flg: true)
end
def fail_second_generation!
allow(TagName).to receive(:generate_name).and_call_original
allow(TagName).to receive(:generate_name)
.with(kind_of(Locale), second, second.tag_name('ja'))
.and_raise('backfill failed')
end
it 'rolls back a newly created locale and the preceding generated names' do
locale = build_locale
fail_second_generation!
expect { locale.save! }.to raise_error('backfill failed')
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
expect(generated_names).to be_empty
expect(first.reload.name('ja')).to eq('activation_first')
expect(second.reload.name('ja')).to eq('activation_second')
end
it 'does not backfill while the locale is inactive' do
build_locale(deprecated_at: Time.current).save!
expect(generated_names).to be_empty
end
it 'backfills missing primary names when the locale becomes active' do
locale = build_locale(deprecated_at: Time.current)
locale.save!
locale.update!(deprecated_at: nil)
expect(generated_names.pluck(:tag_id)).to contain_exactly(first.id, second.id)
expect(generated_names).to all(have_attributes(
script_code: 'Kana', primary_flg: true, auto_generated: true))
end
it 'rolls back activation and all generated names when backfill fails' do
locale = build_locale(deprecated_at: Time.current)
locale.save!
previous_deprecated_at = locale.reload.deprecated_at
fail_second_generation!
expect { locale.update!(deprecated_at: nil) }.to raise_error('backfill failed')
expect(locale.reload.deprecated_at).to eq(previous_deprecated_at)
expect(generated_names).to be_empty
end
it 'preserves a human primary through deactivation and repeated activation' do
locale = build_locale
locale.save!
primary = generated_names.find_by!(tag: first)
primary.update!(name: 'human_confirmed', auto_generated: false)
snapshot = generated_names.order(:id).map(&:attributes)
2.times do
locale.update!(deprecated_at: Time.current)
locale.update!(deprecated_at: nil)
end
expect(generated_names.order(:id).map(&:attributes)).to eq(snapshot)
expect(primary.reload).to have_attributes(name: 'human_confirmed', auto_generated: false)
end
end
+42 -50
ファイルの表示
@@ -1,13 +1,13 @@
require 'rails_helper'
RSpec.describe Locale, type: :model do
include_context 'English locale'
include_context 'Ainu locale'
def create_english_locale!
prepare_english_reference!
described_class.create!(code: 'en', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: '', name: 'English')
def create_ainu_locale!
prepare_ainu_reference!
described_class.create!(code: 'ain-Kana', language_code: 'ain',
script_code: 'Kana', region_code: 392,
variants: '', name: 'Ainu')
end
it 'keeps the active Nipponese locale consistent with its identity tuple' do
@@ -27,7 +27,6 @@ RSpec.describe Locale, type: :model do
end
it 'generates a primary name for every existing tag using the name generator' do
pending '日本語以外の Locale 有効化は別課題で実装する'
first = create(:tag, primary_name: 'first_existing_tag')
second = create(:tag, primary_name: 'second_existing_tag')
allow(TagName).to receive(:generate_name).and_call_original
@@ -35,63 +34,60 @@ RSpec.describe Locale, type: :model do
.with(kind_of(described_class), first, primary_tag_name_for(first, 'ja'))
.and_return('name_from_generator')
locale = create_english_locale!
locale = create_ainu_locale!
expect(TagName).to have_received(:generate_name)
.with(locale, first, primary_tag_name_for(first, 'ja'))
expect(TagName).to have_received(:generate_name)
.with(locale, second, primary_tag_name_for(second, 'ja'))
expect(first.tag_names.find_by!(language_code: 'en', primary_flg: true))
expect(first.tag_names.find_by!(language_code: 'ain', primary_flg: true))
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
language_code: 'en', script_code: 'Latn',
language_code: 'ain', script_code: 'Kana',
primary_flg: true, auto_generated: true)
expect(second.tag_names.find_by!(language_code: 'en', primary_flg: true))
.to have_attributes(tag_id: second.id, language_code: 'en',
script_code: 'Latn', primary_flg: true,
expect(second.tag_names.find_by!(language_code: 'ain', primary_flg: true))
.to have_attributes(tag_id: second.id, language_code: 'ain',
script_code: 'Kana', primary_flg: true,
auto_generated: true)
expect(TagName.where(language_code: 'en', primary_flg: true,
expect(TagName.where(language_code: 'ain', primary_flg: true,
tag_id: [first.id, second.id]).count).to eq(2)
end
it 'does not duplicate an existing primary name in the new language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
prepare_ainu_reference!
tag = create(:tag, primary_name: 'already_named')
existing = create(:tag_name, name: 'existing_english', tag:,
language_code: 'en', script_code: 'Latn')
existing = create(:tag_name, name: 'existing_ainu', tag:,
language_code: 'ain', script_code: 'Kana')
create_english_locale!
create_ainu_locale!
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true))
expect(TagName.where(tag_id: tag.id, language_code: 'ain', primary_flg: true))
.to contain_exactly(existing)
expect(existing.reload).to have_attributes(
tag_id: tag.id, language_code: 'en', script_code: 'Latn',
tag_id: tag.id, language_code: 'ain', script_code: 'Kana',
primary_flg: true, auto_generated: false)
end
it 'creates a primary name when the language has only an alias' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
prepare_ainu_reference!
tag = create(:tag, primary_name: 'alias_only_tag')
alias_name = create(:tag_name, :alias, name: 'alias_en',
tag:,
language_code: 'en', script_code: 'Latn')
language_code: 'ain', script_code: 'Kana')
create_english_locale!
create_ainu_locale!
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
language_code: 'en', script_code: 'Latn', primary_flg: false)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'en',
script_code: 'Latn', primary_flg: true,
language_code: 'ain', script_code: 'Kana', primary_flg: false)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'ain', primary_flg: true)
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'ain',
script_code: 'Kana', primary_flg: true,
auto_generated: true)
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true).count).to eq(1)
expect(TagName.where(tag_id: tag.id, language_code: 'ain', primary_flg: true).count).to eq(1)
end
it 'rolls back the locale and all generated names when backfill fails' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
prepare_ainu_reference!
first = create(:tag, primary_name: 'atomic_first')
second = create(:tag, primary_name: 'atomic_second')
allow(TagName).to receive(:generate_name).and_call_original
@@ -99,31 +95,27 @@ RSpec.describe Locale, type: :model do
.with(kind_of(described_class), second, primary_tag_name_for(second, 'ja'))
.and_raise('backfill failed')
expect { create_english_locale! }.to raise_error('backfill failed')
expect { create_ainu_locale! }.to raise_error('backfill failed')
expect(described_class.exists?(code: 'en')).to be(false)
expect(TagName.where(language_code: 'en', tag_id: [first.id, second.id])).to be_empty
expect(described_class.exists?(code: 'ain-Kana')).to be(false)
expect(TagName.where(language_code: 'ain', tag_id: [first.id, second.id])).to be_empty
end
it 'accepts a stable locale code outside the historical enum values' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
it 'rejects a private locale code outside the supported locale set' do
prepare_ainu_reference!
locale = described_class.create!(
code: 'en-x-rspec', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: 'x-rspec', name: 'English')
expect(locale.reload).to have_attributes(
code: 'en-x-rspec', language_code: 'en', script_code: 'Latn',
region_code: 826, variants: 'x-rspec')
expect {
described_class.create!(
code: 'ain-Kana-x-rspec', language_code: 'ain',
script_code: 'Kana', region_code: 392,
variants: 'x-rspec', name: 'Ainu')
}.to raise_error(ArgumentError)
end
it 'registers the future British English locale tuple' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'registers the supported Ainu locale tuple' do
expect(locale).to have_attributes(code: 'en', language_code: 'en',
script_code: 'Latn', region_code: 826,
expect(locale).to have_attributes(code: 'ain-Kana', language_code: 'ain',
script_code: 'Kana', region_code: 392,
variants: '')
end
end
+3 -4
ファイルの表示
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe Script, type: :model do
include_context 'English locale'
include_context 'Ainu locale'
it 'selects locales by script code, not language code' do
expect(described_class.find('Jpan').locales).to contain_exactly(Locale.nipponese)
@@ -25,11 +25,10 @@ RSpec.describe Script, type: :model do
}
end
it 'selects Latin locales when multilingual support is enabled' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'selects Kana locales when multilingual support is enabled' do
locale
expect(described_class.find('Latn').locales).to contain_exactly(locale)
expect(described_class.find('Kana').locales).to contain_exactly(locale)
expect(described_class.find('Jpan').locales).not_to include(locale)
end
end
+12 -14
ファイルの表示
@@ -95,38 +95,36 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
context 'when only another language has the sanitised name' do
include_context 'English locale'
include_context 'Ainu locale'
it 'keeps both names when sanitisation produces a cross-language match' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag_name, name: 'foobar')
english = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english.update_columns(name: 'foo_bar')
ainu = create(:tag_name, name: 'temporary',
language_code: 'ain', script_code: 'Kana')
ainu.update_columns(name: 'foo_bar')
expect { described_class.apply! }.not_to change(TagName, :count)
expect(english.reload).to have_attributes(name: 'foobar', language_code: 'en')
expect(ainu.reload).to have_attributes(name: 'foobar', language_code: 'ain')
expect(nipponese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
end
it 'renames without merging tags or deleting either language identity' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'foobar')
english_name = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
english_name.update_columns(name: 'foo_bar')
ainu_name = create(:tag_name, name: 'temporary',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name)
ainu_name.update_columns(name: 'foo_bar')
described_class.apply!
expect(english_name.reload).to have_attributes(
name: 'foobar', language_code: 'en', tag_id: english.id)
expect(ainu_name.reload).to have_attributes(
name: 'foobar', language_code: 'ain', tag_id: ainu.id)
expect(primary_tag_name_for(nipponese.reload, 'ja')).to have_attributes(
name: 'foobar', language_code: 'ja', tag_id: nipponese.id)
expect(primary_tag_name_for(english.reload, 'en')).to eq(english_name)
expect(primary_tag_name_for(ainu.reload, 'ain')).to eq(ainu_name)
end
end
+25 -29
ファイルの表示
@@ -1,17 +1,16 @@
require 'rails_helper'
RSpec.describe TagName, type: :model do
include_context 'English locale'
include_context 'Ainu locale'
it 'allows the same name in different languages' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
create(:tag_name, name: 'shared_name')
english = build(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
ainu = build(:tag_name, name: 'shared_name',
language_code: 'ain', script_code: 'Kana')
expect(english).to be_valid
expect { english.save! }.to change(described_class, :count).by(1)
expect(ainu).to be_valid
expect { ainu.save! }.to change(described_class, :count).by(1)
end
it 'rejects a duplicate name within the same language' do
@@ -67,11 +66,10 @@ RSpec.describe TagName, type: :model do
end
it 'completes an ordinary tag factory under future active languages' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = create(:tag, primary_name: 'future_factory')
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
expect(TagName.where(tag:, language_code: 'ain', primary_flg: true))
.to contain_exactly(have_attributes(auto_generated: true))
end
@@ -114,46 +112,44 @@ RSpec.describe TagName, type: :model do
end
it 'resolves the primary name of the alias language on the same tag' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = create(:tag, primary_name: '日本語正本')
english_primary = primary_tag_name_for(tag, 'en')
english_primary.update!(name: 'english_primary', auto_generated: false)
english_alias = create(:tag_name, :alias, tag:, name: 'english_alias',
language_code: 'en', script_code: 'Latn')
ainu_primary = primary_tag_name_for(tag, 'ain')
ainu_primary.update!(name: 'ainu_primary', auto_generated: false)
ainu_alias = create(:tag_name, :alias, tag:, name: 'ainu_alias',
language_code: 'ain', script_code: 'Kana')
representative_id = tag.tag_name('ja')&.id
primary = described_class.find_by!(tag_id: english_alias.tag_id,
language_code: english_alias.language_code,
primary = described_class.find_by!(tag_id: ainu_alias.tag_id,
language_code: ainu_alias.language_code,
primary_flg: true)
expect(primary).to eq(english_primary)
expect(described_class.canonicalise(locale, [english_alias.name]))
.to eq([english_primary.name])
expect(primary).to eq(ainu_primary)
expect(described_class.canonicalise(locale, [ainu_alias.name]))
.to eq([ainu_primary.name])
expect(tag.reload.tag_name('ja')&.id).to eq(representative_id)
end
it 'resolves only aliases in the requested language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'nipponese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
ainu_name = create(:tag_name, name: 'ainu_canonical',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name)
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
ainu_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: ainu,
language_code: 'ain', script_code: 'Kana')
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(ainu_alias.reload).to have_attributes(
tag_id: ainu.id,
primary_flg: false, language_code: 'ain')
expect(described_class.canonicalise(locale, ['shared_alias']))
.to eq(['english_canonical'])
.to eq(['ainu_canonical'])
expect(described_class.canonicalise(Locale.nipponese, ['shared_alias']))
.to eq(['nipponese_canonical'])
end
+63 -74
ファイルの表示
@@ -8,7 +8,7 @@ RSpec.describe Tag, type: :model do
it 'checks the requested language and treats an empty description as present' do
TagDescription.create!(tag:, language_code: 'ja', body: '', version_no: 1)
expect(tag.has_description?(language_code: 'ja')).to be(true)
expect(tag.has_description?(language_code: 'en')).to be(false)
expect(tag.has_description?(language_code: 'ain')).to be(false)
end
it 'returns false without a description' do
@@ -23,16 +23,16 @@ RSpec.describe Tag, type: :model do
subscriber = ->(*args) { queries << args.last.fetch(:sql) }
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') do
expect(loaded.has_description?(language_code: 'ja')).to be(true)
expect(loaded.has_description?(language_code: 'en')).to be(false)
expect(loaded.has_description?(language_code: 'ain')).to be(false)
end
expect(queries).to be_empty
end
it 'keeps language selection generic without enabling another Language record' do
tag.association(:tag_descriptions).target = [
TagDescription.new(tag:, language_code: 'en', body: '', version_no: 1)]
TagDescription.new(tag:, language_code: 'ain', body: '', version_no: 1)]
tag.association(:tag_descriptions).loaded!
expect(tag.has_description?(language_code: 'en')).to be(true)
expect(tag.has_description?(language_code: 'ain')).to be(true)
expect(tag.has_description?(language_code: 'ja')).to be(false)
end
end
@@ -61,33 +61,32 @@ RSpec.describe Tag, type: :model do
end
describe '.normalise_tags!' do
include_context 'English locale'
include_context 'Ainu locale'
it 'canonicalises aliases in the supplied language only' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
prepare_ainu_reference!
nipponese = create(:tag, primary_name: 'nipponese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
primary_tag_name_for(english, 'ja').update!(
name: 'english_in_nipponese', auto_generated: false)
english_locale = locale
ainu_name = create(:tag_name, name: 'ainu_canonical',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name)
primary_tag_name_for(ainu, 'ja').update!(
name: 'ainu_in_nipponese', auto_generated: false)
ainu_locale = locale
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
ainu_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: ainu,
language_code: 'ain', script_code: 'Kana')
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(ainu_alias.reload).to have_attributes(
tag_id: ainu.id,
primary_flg: false, language_code: 'ain')
expect(described_class.normalise_tags!(
english_locale, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([english])
ainu_locale, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([ainu])
expect(described_class.normalise_tags!(
Locale.nipponese, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([nipponese])
@@ -253,129 +252,120 @@ RSpec.describe Tag, type: :model do
describe '.find_or_create_by_tag_name!' do
context 'with an explicit locale' do
include_context 'English locale'
include_context 'Ainu locale'
it 'creates the representative name with locale attributes and ownership' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = described_class.find_or_create_by_tag_name!(
locale, 'english_name', category: :character)
locale, 'ainu_name', category: :character)
expect(primary_tag_name_for(tag, 'en')).to have_attributes(
language_code: 'en', script_code: 'Latn',
expect(primary_tag_name_for(tag, 'ain')).to have_attributes(
language_code: 'ain', script_code: 'Kana',
primary_flg: true, tag_id: tag.id)
expect(tag.category).to eq('character')
end
it 'uses the English primary as the source for a generated Nipponese name' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'uses a Japanese fallback for an Ainu-origin tag without reverse transcription' do
locale
allow(TagName).to receive(:generate_name).and_return('generated_nipponese')
allow(TagName).to receive(:generate_name).and_call_original
tag = described_class.find_or_create_by_tag_name!(
locale, 'english_origin', category: :general)
locale, 'ainu_origin', category: :general)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
expect(TagName.where(tag:, language_code: 'ain', primary_flg: true))
.to contain_exactly(have_attributes(
name: 'english_origin', auto_generated: false))
name: 'ainu_origin', auto_generated: false))
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
.to contain_exactly(have_attributes(
name: 'generated_nipponese', auto_generated: true))
source_tag_name = TagName.find_by!(tag:, language_code: 'en', primary_flg: true)
name: "Tag_##{ tag.id }", auto_generated: true))
expect(TagName).to have_received(:generate_name)
.with(Locale.nipponese, tag, source_tag_name)
.with(Locale.nipponese, tag, nil)
end
it 'gives a new tag one primary name for every active language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'new_multilingual_tag', category: :general)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
expect(TagName.where(tag:, language_code: 'ain', primary_flg: true).count).to eq(1)
end
it 'creates one primary per language when multiple locales use English' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'creates one primary per language when multiple locales use Ainu' do
locale
described_class.find_or_create_by_tag_name!(
locale, 'before_second_english_locale', category: :general)
Locale.create!(code: 'en-x-rspec', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: 'x-rspec', name: 'English')
locale, 'before_second_ainu_locale', category: :general)
tag = described_class.find_or_create_by_tag_name!(
locale, 'one_english_primary', category: :general)
locale, 'one_ainu_primary', category: :general)
TagNameCompletion.for_tag!(tag, locales: [locale, locale.dup, Locale.nipponese])
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
expect(TagName.where(tag:, language_code: 'ain', primary_flg: true).count).to eq(1)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
end
it 'rolls back a new tag and requested name if name generation fails' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
original_tag_count = described_class.count
allow(TagName).to receive(:generate_name).and_raise('generation failed')
expect {
described_class.find_or_create_by_tag_name!(
locale, 'atomic_english_name', category: :general)
locale, 'atomic_ainu_name', category: :general)
}.to raise_error('generation failed')
expect(described_class.count).to eq(original_tag_count)
expect(TagName.where(language_code: 'en', name: 'atomic_english_name')).to be_empty
expect(TagName.where(language_code: 'ain', name: 'atomic_ainu_name')).to be_empty
expect(described_class.joins(:tag_names)
.where(tag_names: { name: 'atomic_english_name' })).to be_empty
.where(tag_names: { name: 'atomic_ainu_name' })).to be_empty
end
it 'creates separate tag identities for the same name in different languages' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'same_name', category: :general)
english = described_class.find_or_create_by_tag_name!(
primary_tag_name_for(nipponese, 'ain').update!(
name: 'other_ainu_name', auto_generated: false)
ainu = described_class.find_or_create_by_tag_name!(
locale, 'same_name', category: :character)
expect(english).not_to eq(nipponese)
expect(primary_tag_name_for(english, 'en')).to have_attributes(
name: 'same_name', tag_id: english.id, language_code: 'en', primary_flg: true)
expect(ainu).not_to eq(nipponese)
expect(primary_tag_name_for(ainu, 'ain')).to have_attributes(
name: 'same_name', tag_id: ainu.id, language_code: 'ain', primary_flg: true)
expect(primary_tag_name_for(nipponese, 'ja')).to have_attributes(
name: 'same_name', tag_id: nipponese.id, language_code: 'ja', primary_flg: true)
expect(described_class.find_or_create_by_tag_name!(
locale, 'same_name', category: :general)).to eq(english)
locale, 'same_name', category: :general)).to eq(ainu)
expect(described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'same_name', category: :general)).to eq(nipponese)
end
it 'preserves the V1 representative when looking up another primary language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = create(:tag, primary_name: '日本語代表名')
representative_id = tag.tag_name('ja')&.id
english = primary_tag_name_for(tag, 'en')
english.update!(name: 'english_primary', auto_generated: false)
ainu = primary_tag_name_for(tag, 'ain')
ainu.update!(name: 'ainu_primary', auto_generated: false)
found = described_class.find_or_create_by_tag_name!(
locale, english.name, category: :general)
locale, ainu.name, category: :general)
expect(found).to eq(tag)
expect(tag.reload.tag_name('ja')&.id).to eq(representative_id)
expect(english.reload.tag_id).to eq(tag.id)
expect(ainu.reload.tag_id).to eq(tag.id)
end
it 'normalises names using the supplied locale' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tags = described_class.normalise_tags!(
locale, ['character:normalised_english'],
locale, ['character:normalised_ainu'],
with_tagme: false, with_no_deerjikist: false)
expect(tags.length).to eq(1)
expect(primary_tag_name_for(tags.first, 'en')).to have_attributes(
name: 'normalised_english', language_code: 'en', script_code: 'Latn',
expect(primary_tag_name_for(tags.first, 'ain')).to have_attributes(
name: 'normalised_ainu', language_code: 'ain', script_code: 'Kana',
primary_flg: true, tag_id: tags.first.id)
end
end
@@ -444,26 +434,25 @@ RSpec.describe Tag, type: :model do
end
context 'with names in multiple languages' do
include_context 'English locale'
include_context 'Ainu locale'
it 'keeps the target primary and moves source names as aliases' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_primary = primary_tag_name_for(target_tag, 'en')
source_primary = primary_tag_name_for(source_tag, 'en')
ainu_primary = primary_tag_name_for(target_tag, 'ain')
source_primary = primary_tag_name_for(source_tag, 'ain')
source_alias = create(:tag_name, :alias, tag: source_tag,
name: 'source_english_alias', language_code: 'en', script_code: 'Latn')
name: 'source_ainu_alias', language_code: 'ain', script_code: 'Kana')
expect([english_primary, source_primary]).to all(have_attributes(
expect([ainu_primary, source_primary]).to all(have_attributes(
primary_flg: true, auto_generated: true))
described_class.merge_tags!(target_tag, [source_tag])
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(english_primary)
expect(TagName.where(tag: target_tag, language_code: 'en', primary_flg: true))
.to contain_exactly(english_primary)
expect(primary_tag_name_for(target_tag.reload, 'ain')).to eq(ainu_primary)
expect(TagName.where(tag: target_tag, language_code: 'ain', primary_flg: true))
.to contain_exactly(ainu_primary)
expect([source_primary.reload, source_alias.reload]).to all(have_attributes(
tag_id: target_tag.id, language_code: 'en', primary_flg: false))
tag_id: target_tag.id, language_code: 'ain', primary_flg: false))
expect(TagName.where(tag: target_tag, language_code: 'ja', primary_flg: true))
.to contain_exactly(primary_tag_name_for(target_tag, 'ja'))
expect(source_tag_name.reload).to have_attributes(
+54
ファイルの表示
@@ -0,0 +1,54 @@
require 'rails_helper'
RSpec.describe 'API locale resolution', type: :request do
let!(:tag) { create(:tag, primary_name: '日本語正本') }
it 'defaults an omitted locale to Japanese' do
get "/tags/#{ tag.id }"
expect(response).to have_http_status(:ok)
expect(json.fetch('name')).to eq('日本語正本')
end
it 'accepts an explicitly requested active Japanese locale' do
get "/tags/#{ tag.id }", params: { locale: 'ja' }
expect(response).to have_http_status(:ok)
expect(json.fetch('name')).to eq('日本語正本')
end
['', 'en', 'ain', 'Jpan', 'JA', 'unknown', ['ja']].each do |code|
it "rejects invalid locale #{ code.inspect } instead of defaulting to Japanese" do
get "/tags/#{ tag.id }", params: { locale: code }
expect(response).to have_http_status(:not_found)
expect(response.body).not_to include('日本語正本')
end
end
it 'rejects a supported locale that has not been registered' do
get "/tags/#{ tag.id }", params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:not_found)
end
it 'rejects an inactive locale' do
Locale.nipponese.update!(deprecated_at: Time.current)
get "/tags/#{ tag.id }", params: { locale: 'ja' }
expect(response).to have_http_status(:not_found)
end
it 'does not write posts or tag names under an invalid locale' do
sign_in_as(create_member_user!)
expect do
post '/posts', params: {
locale: 'en', title: 'invalid locale', url: 'https://example.com/invalid-locale',
tags: 'unwritten_tag' }
end.to change(Post, :count).by(0).and change(TagName, :count).by(0)
expect(response).to have_http_status(:not_found)
end
end
+61 -78
ファイルの表示
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe 'Locale propagation on write paths', type: :request do
include_context 'English locale'
include_context 'Ainu locale'
let(:member) { create(:user, :member) }
@@ -54,13 +54,12 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
end
['en', nil].each do |requested_locale|
['ain-Kana', nil].each do |requested_locale|
context "with locale #{ requested_locale.inspect }" do
let(:locale_params) { requested_locale ? { locale: requested_locale } : { } }
let(:expected_locale) { requested_locale ? locale : Locale.nipponese }
it 'creates post tags in the requested language or Nipponese fallback' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
post '/posts', params: locale_params.merge(
title: 'Locale post', url: 'https://example.com/locale-post',
@@ -72,7 +71,6 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
it 'updates post tags in the requested language or Nipponese fallback' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
record = create(:post)
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
@@ -87,7 +85,6 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
it 'passes the resolved locale to bulk creation' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
# Worker propagation is exercised in post_bulk_creator_spec.
expect_any_instance_of(PostBulkCreator).to receive(:run)
@@ -102,7 +99,6 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
it 'creates materials with tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
post '/materials', params: locale_params.merge(
tag: 'locale_material', url: 'https://example.com/material')
@@ -113,7 +109,6 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
it 'updates materials with tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
tag = create(:tag, category: :material)
material = Material.create!(tag:, url: 'https://example.com/material')
@@ -127,7 +122,6 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
it 'links Nico external tags to internal tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
external = create(:external_tag)
NicoTagVersionRecorder.record!(
@@ -141,7 +135,6 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
it 'creates parent tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
tag = create(:tag)
@@ -157,10 +150,10 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
context 'with existing names in both languages' do
let(:english_name) do
create(:tag_name, name: 'shared_name', language_code: 'en', script_code: 'Latn')
let(:ainu_name) do
create(:tag_name, name: 'shared_name', language_code: 'ain', script_code: 'Kana')
end
let(:english_tag) { create(:tag, primary_tag_name: english_name, category: :material) }
let(:ainu_tag) { create(:tag, primary_tag_name: ainu_name, category: :material) }
let(:nipponese_tag) do
create(:tag, primary_name: 'temporary_nipponese', category: :general).tap do |tag|
# Isolate lookup from the separately tested uniqueness validation.
@@ -168,173 +161,163 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
end
end
it 'plans and creates the post using the English identity and category' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'plans and creates the post using the Ainu identity and category' do
locale
english_tag
ainu_tag
nipponese_tag
post '/posts', params: {
locale: 'en', title: 'English identity',
url: 'https://example.com/english-identity', tags: 'shared_name', parent_post_ids: '' }
locale: 'ain-Kana', title: 'Ainu identity',
url: 'https://example.com/ainu-identity', tags: 'shared_name', parent_post_ids: '' }
expect(response).to have_http_status(:created), response.body
record = Post.find(json.fetch('id'))
expect(record.tags).to include(english_tag)
expect(record.tags).to include(ainu_tag)
expect(record.tags).not_to include(nipponese_tag)
expect(english_tag.reload.category).to eq('material')
expect(ainu_tag.reload.category).to eq('material')
expect(nipponese_tag.reload.category).to eq('general')
end
it 'updates the post using the English identity' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'updates the post using the Ainu identity' do
locale
english_tag
ainu_tag
nipponese_tag
record = create(:post)
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
put "/posts/#{ record.id }", params: {
locale: 'en', title: record.title, tags: 'shared_name', parent_post_ids: '',
locale: 'ain-Kana', title: record.title, tags: 'shared_name', parent_post_ids: '',
base_version_no: record.reload.version_no }
expect(response).to have_http_status(:ok), response.body
expect(record.reload.tags).to include(english_tag)
expect(record.reload.tags).to include(ainu_tag)
expect(record.tags).not_to include(nipponese_tag)
end
it 'resolves the English material tag' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'resolves the Ainu material tag' do
locale
english_tag
ainu_tag
nipponese_tag
post '/materials', params: {
locale: 'en', tag: 'shared_name', url: 'https://example.com/english-material' }
locale: 'ain-Kana', tag: 'shared_name', url: 'https://example.com/ainu-material' }
expect(response).to have_http_status(:created), response.body
expect(Material.find(json.fetch('id')).tag).to eq(english_tag)
expect(Material.find(json.fetch('id')).tag).to eq(ainu_tag)
expect(Material.where(tag: nipponese_tag)).to be_empty
end
it 'resolves the English parent tag' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'resolves the Ainu parent tag' do
locale
english_tag
ainu_tag
nipponese_tag
child = create(:tag)
put "/tags/#{ child.id }", params: {
locale: 'en', name: primary_tag_name_for(child, 'ja').name,
locale: 'ain-Kana', name: primary_tag_name_for(child, 'ja').name,
category: child.category, deprecated: false,
aliases: '', parent_tags: 'shared_name' }
expect(response).to have_http_status(:ok), response.body
expect(TagImplication.where(tag: child).pluck(:parent_tag_id)).to eq([english_tag.id])
expect(TagImplication.where(tag: child).pluck(:parent_tag_id)).to eq([ainu_tag.id])
end
it 'resolves the English internal tag for a Nico link' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'resolves the Ainu internal tag for a Nico link' do
locale
english_tag
ainu_tag
nipponese_tag
external = create(:external_tag)
NicoTagVersionRecorder.record!(
external_tag: external, event_type: :create, created_by_user: member)
put "/tags/nico/#{ external.id }", params: { locale: 'en', tags: 'shared_name' }
put "/tags/nico/#{ external.id }", params: { locale: 'ain-Kana', tags: 'shared_name' }
expect(response).to have_http_status(:ok), response.body
expect(external.reload.linked_tags).to contain_exactly(english_tag)
expect(external.reload.linked_tags).to contain_exactly(ainu_tag)
end
end
context 'when renaming or updating aliases in English' do
let(:english_name) do
create(:tag_name, name: 'english_original', language_code: 'en', script_code: 'Latn')
context 'when renaming or updating aliases in Ainu' do
let(:ainu_name) do
create(:tag_name, name: 'ainu_original', language_code: 'ain', script_code: 'Kana')
end
let(:tag) { create(:tag, primary_tag_name: english_name) }
let(:tag) { create(:tag, primary_tag_name: ainu_name) }
it 'marks a human-renamed generated primary as no longer auto-generated' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
generated_tag = create(:tag, primary_name: 'rename_generated_source')
generated_name = primary_tag_name_for(generated_tag, 'en')
generated_name = primary_tag_name_for(generated_tag, 'ain')
expect(generated_name.auto_generated).to be(true)
patch "/tags/#{ generated_tag.id }", params: {
locale: 'en', name: 'human_english_name' }
locale: 'ain-Kana', name: 'human_ainu_name' }
expect(response).to have_http_status(:ok), response.body
expect(generated_name.reload).to have_attributes(
name: 'human_english_name', auto_generated: false)
name: 'human_ainu_name', auto_generated: false)
end
it 'does not steal another tag primary for an alias' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
other_tag = create(:tag, primary_name: 'other_nipponese')
other_primary = primary_tag_name_for(other_tag, 'en')
other_primary.update!(name: 'other_english_primary', auto_generated: false)
create(:tag_name, :alias, tag:, name: 'existing_english_alias',
language_code: 'en', script_code: 'Latn')
primary_name_before = primary_tag_name_for(tag, 'en').name
other_primary = primary_tag_name_for(other_tag, 'ain')
other_primary.update!(name: 'other_ainu_primary', auto_generated: false)
create(:tag_name, :alias, tag:, name: 'existing_ainu_alias',
language_code: 'ain', script_code: 'Kana')
primary_name_before = primary_tag_name_for(tag, 'ain').name
aliases_before =
tag.tag_names.where(language_code: 'en', primary_flg: false)
tag.tag_names.where(language_code: 'ain', primary_flg: false)
.order(:name).pluck(:name)
category_before = tag.category
put "/tags/#{ tag.id }", params: {
locale: 'en', name: 'renamed_before_alias_collision',
locale: 'ain-Kana', name: 'renamed_before_alias_collision',
category: 'character', deprecated: false,
aliases: 'other_english_primary', parent_tags: '' }
aliases: 'other_ainu_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(other_primary.reload).to have_attributes(
tag_id: other_tag.id, language_code: 'en', primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'en', primary_flg: true))
tag_id: other_tag.id, language_code: 'ain', primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'ain', primary_flg: true))
.to contain_exactly(other_primary)
expect(primary_tag_name_for(tag.reload, 'en').name).to eq(primary_name_before)
expect(primary_tag_name_for(tag.reload, 'ain').name).to eq(primary_name_before)
expect(tag.category).to eq(category_before)
aliases_after =
tag.tag_names.where(language_code: 'en', primary_flg: false)
tag.tag_names.where(language_code: 'ain', primary_flg: false)
.order(:name).pluck(:name)
expect(aliases_after).to eq(aliases_before)
end
end
it 'allows a rename to a name already used in Nipponese' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'rename_target')
patch "/tags/#{ tag.id }", params: { locale: 'en', name: 'rename_target' }
patch "/tags/#{ tag.id }", params: { locale: 'ain-Kana', name: 'rename_target' }
expect(response).to have_http_status(:ok), response.body
expect(primary_tag_name_for(tag.reload, 'en')).to have_attributes(
name: 'rename_target', language_code: 'en', tag_id: tag.id)
expect(primary_tag_name_for(tag.reload, 'ain')).to have_attributes(
name: 'rename_target', language_code: 'ain', tag_id: tag.id)
expect(primary_tag_name_for(nipponese.reload, 'ja')).to have_attributes(
name: 'rename_target', language_code: 'ja', tag_id: nipponese.id)
end
it 'creates aliases in the requested language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
put "/tags/#{ tag.id }", params: {
locale: 'en', name: english_name.name,
locale: 'ain-Kana', name: ainu_name.name,
category: tag.category, deprecated: false,
aliases: 'english_alias', parent_tags: '' }
aliases: 'ainu_alias', parent_tags: '' }
expect(response).to have_http_status(:ok), response.body
expect(TagName.find_by!(language_code: 'en', name: 'english_alias'))
expect(TagName.find_by!(language_code: 'ain', name: 'ainu_alias'))
.to have_attributes(
script_code: 'Latn',
tag_id: tag.id, primary_flg: false, language_code: 'en')
primary = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
expect(primary).to have_attributes(name: 'english_original', script_code: 'Latn')
script_code: 'Kana',
tag_id: tag.id, primary_flg: false, language_code: 'ain')
primary = TagName.find_by!(tag_id: tag.id, language_code: 'ain', primary_flg: true)
expect(primary).to have_attributes(name: 'ainu_original', script_code: 'Kana')
end
it 'does not take an alias from another language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'nipponese_owner')
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
@@ -344,7 +327,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
primary_flg: false, language_code: 'ja')
put "/tags/#{ tag.id }", params: {
locale: 'en', name: english_name.name,
locale: 'ain-Kana', name: ainu_name.name,
category: tag.category, deprecated: false,
aliases: 'shared_alias', parent_tags: '' }
@@ -352,12 +335,12 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(TagName.find_by!(language_code: 'en', name: 'shared_alias'))
expect(TagName.find_by!(language_code: 'ain', name: 'shared_alias'))
.to have_attributes(
tag_id: tag.id, script_code: 'Latn',
primary_flg: false, language_code: 'en')
tag_id: tag.id, script_code: 'Kana',
primary_flg: false, language_code: 'ain')
expect(TagName.where(name: 'shared_alias').pluck(:language_code, :tag_id))
.to contain_exactly(['ja', nipponese.id], ['en', tag.id])
.to contain_exactly(['ja', nipponese.id], ['ain', tag.id])
end
end
end
+16 -15
ファイルの表示
@@ -22,7 +22,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
it 'uses selected-language names in native collection and nested placements' do
["/tag_groups/#{ group.id }", '/tag_groups'].each do |path|
get path, params: { locale: 'ain' }
get path, params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:ok)
representation = json.is_a?(Array) ? json.sole : json
expect(representation.fetch('members').sole.fetch('tag').fetch('name'))
@@ -37,14 +37,14 @@ RSpec.describe 'Tag group locale contracts', type: :request do
it 'uses snapshot names recursively for history and current names for the latest view' do
version = group.versions.sole
ainu_name.update!(name: 'renamed_ainu')
get "/wiki/#{ group.id }", params: { locale: 'ain' }
get "/wiki/#{ group.id }", params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:ok)
expect(json.fetch('body')).to include('renamed_ainu')
get "/wiki/#{ group.id }", params: { locale: 'ain', version: version.id }
get "/wiki/#{ group.id }", params: { locale: 'ain-Kana', version: version.id }
expect(response).to have_http_status(:ok)
expect(json.fetch('body').scan('ainu_member').length).to eq(2)
expect(json.fetch('body')).not_to include('renamed_ainu', 'japanese_member')
get "/wiki/#{ group.id }/diff", params: { locale: 'ain', from: version.id }
get "/wiki/#{ group.id }/diff", params: { locale: 'ain-Kana', from: version.id }
expect(response).to have_http_status(:ok)
expect(json.fetch('diff').map { _1.fetch('type') }.uniq).to eq(['context'])
end
@@ -52,23 +52,24 @@ RSpec.describe 'Tag group locale contracts', type: :request do
['タググループ:', 'tag_group:', 'TAG_GROUP:'].each do |prefix|
it "round trips #{ prefix } through create, lookup, search and update" do
post '/wiki', params: {
locale: 'ain', title: "#{ prefix }parent", body: '- tag_group:locale group' },
locale: 'ain-Kana', title: "#{ prefix }parent", body: '- tag_group:locale group' },
headers: headers
expect(response).to have_http_status(:created)
id = json.fetch('id')
expect(json.fetch('body')).to include('タググループ:locale group')
path = "/wiki/title/#{ CGI.escapeURIComponent("#{ prefix }parent") }"
get path, params: { locale: 'ain' }
get path, params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:ok)
expect(json.fetch('id')).to eq(id)
get "#{ path }/exists", params: { locale: 'ain' }
get "#{ path }/exists", params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:no_content)
get '/wiki/search', params: { locale: 'ain', title: "#{ prefix }parent" }
get '/wiki/search', params: { locale: 'ain-Kana', title: "#{ prefix }parent" }
expect(json.map { _1.fetch('id') }).to eq([id])
put "/wiki/#{ id }", params: {
locale: 'ain', title: "#{ prefix }renamed", body: '- ainu_member' }, headers: headers
locale: 'ain-Kana', title: "#{ prefix }renamed", body: '- ainu_member' },
headers: headers
expect(response).to have_http_status(:ok)
get "/tag_groups/#{ id }", params: { locale: 'ain' }
get "/tag_groups/#{ id }", params: { locale: 'ain-Kana' }
expect(json.fetch('members').sole.fetch('tag').fetch('name')).to eq('ainu_member')
end
end
@@ -81,7 +82,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
end.to raise_error(TagGroupMarkdownParser::InvalidDocument)
before_state = group_state
put "/wiki/#{ group.id }", params: {
locale: 'ain', title: 'タググループ:partial rename', body: '- japanese_member' },
locale: 'ain-Kana', title: 'タググループ:partial rename', body: '- japanese_member' },
headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(group_state).to eq(before_state)
@@ -89,15 +90,15 @@ RSpec.describe 'Tag group locale contracts', type: :request do
it 'passes locale into native validation and facade compatibility checks' do
post '/tag_groups', params: {
locale: 'ain', name: 'native', description: nil, members:, placements: },
locale: 'ain-Kana', name: 'native', description: nil, members:, placements: },
headers:, as: :json
expect(response).to have_http_status(:created)
id = json.fetch('id')
expect(json.fetch('members').sole.fetch('tag').fetch('name')).to eq('ainu_member')
get "/wiki/#{ id }", params: { locale: 'ain' }
get "/wiki/#{ id }", params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:ok)
body = json.fetch('body')
put "/wiki/#{ id }", params: { locale: 'ain', title: 'tag_group:native', body: },
put "/wiki/#{ id }", params: { locale: 'ain-Kana', title: 'tag_group:native', body: },
headers: headers
expect(response).to have_http_status(:ok)
expect(TagGroup.find(id).version_no).to eq(1)
@@ -115,7 +116,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
representation = TagGroupRepr.historical(ainu_locale, version)
expect(representation.fetch('members').sole.fetch('tag').fetch('name')).to be_nil
expect(representation.fetch('placements').sole.fetch('tag').fetch('name')).to be_nil
get "/wiki/#{ old_group.id }", params: { locale: 'ain', version: version.id }
get "/wiki/#{ old_group.id }", params: { locale: 'ain-Kana', version: version.id }
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).not_to include('historical_japanese', 'current_ainu')
end
+20 -20
ファイルの表示
@@ -20,12 +20,12 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
['/wiki', '/wiki/search'].each do |path|
it "filters #{ path } by the selected language" do
get path, params: { locale: 'ain' }
get path, params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:ok)
expect(json.map { _1.fetch('id') }).to eq([ainu.id])
expect(json.sole).to include('title' => 'ainu_title', 'body' => 'Ainu body')
get path, params: { locale: 'ain', title: 'japanese' }
get path, params: { locale: 'ain-Kana', title: 'japanese' }
expect(response).to have_http_status(:ok)
expect(json).to eq([])
end
@@ -39,17 +39,17 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
it 'resolves titles and aliases only in the selected language in both APIs' do
['/wiki/title/japanese_title', '/tags/name/japanese_title/description'].each do |path|
get path, params: { locale: 'ain' }
get path, params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:not_found)
end
['/wiki/title/ainu_title', '/tags/name/ainu_title/description'].each do |path|
get path, params: { locale: 'ain' }
get path, params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:ok)
expect(json.fetch('body')).to eq('Ainu body')
end
get '/wiki/title/ainu_title/exists', params: { locale: 'ain' }
get '/wiki/title/ainu_title/exists', params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:no_content)
get '/wiki/title/japanese_title/exists', params: { locale: 'ain' }
get '/wiki/title/japanese_title/exists', params: { locale: 'ain-Kana' }
expect(response).to have_http_status(:not_found)
end
@@ -57,14 +57,14 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
revision = current_description_version(japanese)
["/wiki/#{ japanese.id }", "/wiki/#{ japanese.id }/exists",
"/wiki/#{ japanese.id }/diff"].each do |path|
get path, params: { locale: 'ain', from: revision.id }
get path, params: { locale: 'ain-Kana', from: revision.id }
expect(response).to have_http_status(:not_found)
expect(response.body).not_to include('Japanese body')
end
get '/wiki/changes', params: { locale: 'ain', id: japanese.id }
get '/wiki/changes', params: { locale: 'ain-Kana', id: japanese.id }
expect(response).to have_http_status(:ok)
expect(json).to eq([])
get '/wiki/changes', params: { locale: 'ain' }
get '/wiki/changes', params: { locale: 'ain-Kana' }
expect(json.map { _1.fetch('revision_id') })
.to eq([current_description_version(ainu).id])
end
@@ -73,7 +73,7 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
snapshot = [japanese.attributes, ainu.attributes, ainu_name.attributes]
expect do
put "/wiki/#{ japanese.id }", params: {
locale: 'ain', title: 'renamed', body: 'wrong target' }, headers: headers
locale: 'ain-Kana', title: 'renamed', body: 'wrong target' }, headers: headers
end.not_to change(TagDescriptionVersion, :count)
expect(response).to have_http_status(:not_found)
expect([japanese.reload.attributes, ainu.reload.attributes, ainu_name.reload.attributes])
@@ -89,10 +89,10 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
snapshot = original.attributes
expect do
if api == :wiki
post '/wiki', params: { locale: 'ain', title: 'other_ainu', body: 'created' },
post '/wiki', params: { locale: 'ain-Kana', title: 'other_ainu', body: 'created' },
headers: headers
else
put "/tags/#{ other.id }/description", params: { locale: 'ain', body: 'created' },
put "/tags/#{ other.id }/description", params: { locale: 'ain-Kana', body: 'created' },
headers: headers
end
end.to change(TagDescription, :count).by(1)
@@ -108,20 +108,20 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
it 'shares selected-language edits and revision identity across both APIs' do
snapshot = japanese.attributes
first = current_description_version(ainu)
put "/tags/#{ tag.id }/description", params: { locale: 'ain', body: 'native edit' },
put "/tags/#{ tag.id }/description", params: { locale: 'ain-Kana', body: 'native edit' },
headers: headers
expect(response).to have_http_status(:ok)
second = current_description_version(ainu)
expect(second).to have_attributes(language_code: 'ain', version_no: 2)
expect(second.id).not_to eq(first.id)
get "/wiki/#{ ainu.id }", params: { locale: 'ain' }
get "/wiki/#{ ainu.id }", params: { locale: 'ain-Kana' }
expect(json).to include('body' => 'native edit', 'revision_id' => second.id)
put "/wiki/#{ ainu.id }", params: {
locale: 'ain', title: 'ainu_title', body: 'facade edit',
locale: 'ain-Kana', title: 'ainu_title', body: 'facade edit',
base_revision_id: second.id }, headers: headers
expect(response).to have_http_status(:ok)
get "/tags/#{ tag.id }/description", params: { locale: 'ain' }
get "/tags/#{ tag.id }/description", params: { locale: 'ain-Kana' }
expect(json).to include('body' => 'facade edit', 'language_code' => 'ain', 'version_no' => 3)
expect(japanese.reload.attributes).to eq(snapshot)
end
@@ -133,7 +133,7 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
snapshot = [ainu.reload.attributes, ainu_name.attributes, japanese.attributes]
expect do
put "/wiki/#{ ainu.id }", params: {
locale: 'ain', title: 'conflicting_name', body: 'current',
locale: 'ain-Kana', title: 'conflicting_name', body: 'current',
base_revision_id: stale.id }, headers: headers
end.to change(TagDescriptionVersion, :count).by(0).and change(TagVersion, :count).by(0)
expect(response).to have_http_status(:conflict)
@@ -145,14 +145,14 @@ RSpec.describe 'Wiki and native API locale contracts', type: :request do
first = current_description_version(ainu)
TagDescriptionOperation.update!(
ainu_locale, tag:, body: 'later', updated_by_user: user)
get "/wiki/#{ ainu.id }", params: { locale: 'ain', version: first.id }
get "/wiki/#{ ainu.id }", params: { locale: 'ain-Kana', version: first.id }
expect(response).to have_http_status(:ok)
expect(json).to include('body' => 'Ainu body', 'revision_id' => first.id)
get "/wiki/#{ ainu.id }/diff", params: { locale: 'ain', from: first.id }
get "/wiki/#{ ainu.id }/diff", params: { locale: 'ain-Kana', from: first.id }
expect(response).to have_http_status(:ok)
expect(json.fetch('newer_revision_id')).to eq(current_description_version(ainu).id)
get "/wiki/#{ ainu.id }/diff", params: {
locale: 'ain', from: current_description_version(japanese).id }
locale: 'ain-Kana', from: current_description_version(japanese).id }
expect(response).to have_http_status(:not_found)
end
end
+2 -3
ファイルの表示
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe PostBulkCreator do
include_context 'English locale'
include_context 'Ainu locale'
it 'limits workers to two and keeps failures in their request slots' do
locale = Locale.nipponese
@@ -67,8 +67,7 @@ RSpec.describe PostBulkCreator do
expect(results[3].dig(:post, :id)).to eq(4)
end
it 'passes a future English locale to preflight and creation in every worker' do
pending '日本語以外の Locale 有効化は別課題で実装する'
it 'passes a future Ainu locale to preflight and creation in every worker' do
locale
actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123).and_return(actor)
+19 -21
ファイルの表示
@@ -11,23 +11,22 @@ RSpec.describe PostCreatePlan do
end
context 'with an explicit locale' do
include_context 'English locale'
include_context 'Ainu locale'
it 'resolves the matching language identity and its parents' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_name = create(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name, category: :character)
ainu_name = create(:tag_name, name: 'shared_name',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name, category: :character)
nipponese = create(:tag, primary_name: 'temporary_nipponese', category: :general)
# The DB permits this identity; validation has its own contract spec.
primary_tag_name_for(nipponese, 'ja').update_columns(name: 'shared_name')
english_parent_name = create(:tag_name, name: 'english_parent',
language_code: 'en', script_code: 'Latn')
english_parent = create(:tag, primary_tag_name: english_parent_name,
ainu_parent_name = create(:tag_name, name: 'ainu_parent',
language_code: 'ain', script_code: 'Kana')
ainu_parent = create(:tag, primary_tag_name: ainu_parent_name,
category: :material)
nipponese_parent = create(:tag, primary_name: 'nipponese_parent', category: :general)
TagImplication.create!(tag: english, parent_tag: english_parent)
TagImplication.create!(tag: ainu, parent_tag: ainu_parent)
TagImplication.create!(tag: nipponese, parent_tag: nipponese_parent)
plan = described_class.new(attributes: { tags: 'shared_name' }).build!(locale)
@@ -35,35 +34,34 @@ RSpec.describe PostCreatePlan do
expect(plan[:direct_tag_specs]).to eq(
[{ name: 'shared_name', category: :character }])
expect(plan[:post_tag_specs]).to include(
{ name: primary_tag_name_for(english_parent, 'en').name, category: :material })
{ name: primary_tag_name_for(ainu_parent, 'ain').name, category: :material })
expect(plan[:post_tag_specs].pluck(:name))
.not_to include(primary_tag_name_for(nipponese_parent, 'ja').name)
end
it 'resolves aliases within the requested language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'nipponese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name, category: :character)
ainu_name = create(:tag_name, name: 'ainu_canonical',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name, category: :character)
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
ainu_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: ainu,
language_code: 'ain', script_code: 'Kana')
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(ainu_alias.reload).to have_attributes(
tag_id: ainu.id,
primary_flg: false, language_code: 'ain')
plan = described_class.new(attributes: { tags: 'shared_alias' }).build!(locale)
expect(plan[:direct_tag_specs]).to eq(
[{ name: english_name.name, category: :character }])
[{ name: ainu_name.name, category: :character }])
end
end
+3 -4
ファイルの表示
@@ -77,11 +77,10 @@ RSpec.describe PostCreator do
end
context 'with an explicit locale' do
include_context 'English locale'
include_context 'Ainu locale'
[false, true].each do |planned|
it "passes locale to tag creation with planned attributes: #{ planned }" do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
attributes = {
title: 'Locale propagation',
@@ -97,8 +96,8 @@ RSpec.describe PostCreator do
expect(Tag).to have_received(:find_or_create_by_tag_name!)
.with(locale, 'locale_character', category: :character).at_least(:once)
tag_name = TagName.find_by!(language_code: 'en', name: 'locale_character')
expect(tag_name).to have_attributes(script_code: 'Latn', primary_flg: true)
tag_name = TagName.find_by!(language_code: 'ain', name: 'locale_character')
expect(tag_name).to have_attributes(script_code: 'Kana', primary_flg: true)
expect(post.tags).to include(tag_name.tag)
end
end
+68
ファイルの表示
@@ -0,0 +1,68 @@
require 'rails_helper'
RSpec.describe TagNameCompletion do
let!(:language) { Language.create!(code: 'ain', name: 'ain') }
let!(:script) { Script.create!(code: 'Kana', name: 'Kana') }
let(:locale) do
Locale.create!(code: 'ain-Kana', language_code: 'ain', script_code: 'Kana',
region_code: 392, variants: '', name: 'ain-Kana')
end
it 'generates a phonetic primary with the required identity and provenance' do
tag = create(:tag, primary_name: '漢字あカ')
locale
expect(tag.reload.tag_name('ain')).to have_attributes(
name: 'カンチ_ア_カ', language_code: 'ain', script_code: 'Kana',
primary_flg: true, auto_generated: true, tag_id: tag.id)
end
it 'uses the exact fallback when the input cannot be converted' do
tag = create(:tag, primary_name: '😀')
locale
expect(tag.reload.name('ain')).to eq("Tag_##{ tag.id }")
end
it 'uses the exact fallback when a generated name collides with a human name' do
other = create(:tag, primary_name: 'other')
create(:tag_name, tag: other, name: 'AbC', language_code: 'ain', script_code: 'Kana')
tag = create(:tag, primary_name: 'AbC')
locale
expect(tag.reload.name('ain')).to eq("Tag_##{ tag.id }")
expect(other.reload.name('ain')).to eq('AbC')
end
it 'uses the exact fallback for a transcription that fails name validation' do
tag = create(:tag, primary_name: 'base')
source = TagName.new(name: 'nico:invalid', language_code: 'ja')
expect(TagName.generate_name(locale, tag, source)).to eq("Tag_##{ tag.id }")
end
it 'preserves human confirmation and is idempotent across repeated completion' do
locale
tag = Tag.find_or_create_by_tag_name!(Locale.nipponese, 'あいぬ', category: :general)
primary = tag.tag_name('ain')
primary.update!(name: 'human_confirmed', auto_generated: false)
snapshot = tag.tag_names.order(:id).map(&:attributes)
2.times { described_class.for_tag!(tag) }
expect(tag.tag_names.order(:id).map(&:attributes)).to eq(snapshot)
end
it 'creates one primary per active language through the production creation path' do
locale
tag = Tag.find_or_create_by_tag_name!(Locale.nipponese, 'アイヌ', category: :general)
expect(tag.tag_names.where(primary_flg: true).pluck(:language_code, :name))
.to contain_exactly(['ja', 'アイヌ'], ['ain', 'アイヌ'])
expect(Tag.find_or_create_by_tag_name!(locale, 'アイヌ', category: :general)).to eq(tag)
end
end
+34
ファイルの表示
@@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe TagNameTranscription do
{
'AbCdXYZ' => 'AbCdXYZ',
'あいぬ' => 'アイヌ',
'アイヌ' => 'アイヌ',
'日本語' => 'ニチホンコ',
'漢字' => 'カンチ',
'漢あカ' => 'カン_ア_カ',
'漢字あいうカナ' => 'カンチ_アイウ_カナ',
'Ab漢あカZ' => 'Abカン_ア_カZ',
'りゃリュリョ' => 'リヤ_リユリヨ',
'キャット' => 'キヤット',
'コーヒー' => 'コヒ',
'コウコオケイケエカア' => 'コウコオケイケエカア',
'ガギグゲゴダヂヅデド' => 'カキクケコタチチュテト',
'バビブベボパピプペポ' => 'パピプペポパピプペポ',
'ザジズゼゾ' => 'チャチチュチェチョ',
'ジャジュジョ' => 'チヤチユチヨ',
'アㇰアㇱアッアンアㇺアㇷ゚' => 'アㇰアㇱアッアンアㇺアㇷ゚',
'アㇻイㇼウㇽエㇾオㇿ' => 'アㇻイㇼウㇽエㇾオㇿ',
'漢Aあ' => 'カンAア' }.each do |source, expected|
it "transcribes #{ source } as #{ expected }" do
expect(described_class.ainu(source)).to eq(expected)
end
end
['😀', 'ヴ', 'ティ', 'ㇵ', '魑', '𠮷', 'ー', '々'].each do |source|
it "rejects unsupported input #{ source }" do
expect { described_class.ainu(source) }.to raise_error(described_class::Unconvertible)
end
end
end
+14 -17
ファイルの表示
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe TagVersionRecorder do
include_context 'English locale'
include_context 'Ainu locale'
let(:user) { create_member_user! }
let!(:tag) { create(:tag, primary_name: '日本語名') }
@@ -17,11 +17,11 @@ RSpec.describe TagVersionRecorder do
{ 'name' => 'alias_z', 'script_code' => 'Jpan' }] }
end
def prepare_english_names!
def prepare_ainu_names!
locale
primary_tag_name_for(tag, 'en').update!(name: 'English name', auto_generated: false)
create(:tag_name, :alias, tag:, language_code: 'en', script_code: 'Latn',
name: 'English alias')
primary_tag_name_for(tag, 'ain').update!(name: 'Ainu name', auto_generated: false)
create(:tag_name, :alias, tag:, language_code: 'ain', script_code: 'Kana',
name: 'Ainu alias')
end
it 'snapshots the Nipponese primary and sorted aliases in grouped names' do
@@ -62,35 +62,32 @@ RSpec.describe TagVersionRecorder do
end
it 'snapshots future languages together without a request locale argument' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_names!
prepare_ainu_names!
version = described_class.record!(tag:, event_type: :create, created_by_user: user)
expect(version.names).to eq(
'ja' => nipponese_snapshot,
'en' => { 'name' => 'English name', 'script_code' => 'Latn',
'ain' => { 'name' => 'Ainu name', 'script_code' => 'Kana',
'auto_generated' => false,
'aliases' => [{ 'name' => 'English alias', 'script_code' => 'Latn' }] })
'aliases' => [{ 'name' => 'Ainu alias', 'script_code' => 'Kana' }] })
end
it 'creates a version for an English-only change and preserves prior names' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_names!
it 'creates a version for an Ainu-only change and preserves prior names' do
prepare_ainu_names!
original = described_class.record!(tag:, event_type: :create, created_by_user: user)
primary_tag_name_for(tag, 'en').update!(name: 'Renamed')
primary_tag_name_for(tag, 'ain').update!(name: 'Renamed')
updated = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(updated.version_no).to eq(2)
expect(original.reload.names.fetch('en')).to include('name' => 'English name')
expect(updated.names.fetch('en')).to include('name' => 'Renamed')
expect(original.reload.names.fetch('ain')).to include('name' => 'Ainu name')
expect(updated.names.fetch('ain')).to include('name' => 'Renamed')
expect(updated.names.fetch('ja')).to eq(nipponese_snapshot)
end
it 'does not create a version for an unchanged multilingual snapshot' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_names!
prepare_ainu_names!
described_class.record!(tag:, event_type: :create, created_by_user: user)
unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user)
+7 -4
ファイルの表示
@@ -1,19 +1,22 @@
RSpec.shared_context 'Ainu locale contract' do
let(:ainu_locale) { instance_double(Locale, language_code: 'ain', script_code: 'Jpan') }
let(:ainu_locale) do
instance_double(Locale, language_code: 'ain', script_code: 'Kana', deprecated_at: nil)
end
before do
# Only the foreign-key reference is seeded; no Locale or enum is enabled.
# Keep description contracts independent of locale activation and backfill.
Language.connection.execute(<<~SQL)
INSERT IGNORE INTO languages (code, name, created_at)
VALUES ('ain', 'Ainu', CURRENT_TIMESTAMP)
SQL
Script.find_or_create_by!(code: 'Kana') { _1.name = 'Kana' }
allow(Locale).to receive(:find_by).and_call_original
allow(Locale).to receive(:find_by).with(code: 'ain').and_return(ainu_locale)
allow(Locale).to receive(:find_by).with(code: 'ain-Kana').and_return(ainu_locale)
end
def add_ainu_name tag, name
TagName.insert_all!([{
tag_id: tag.id, language_code: 'ain', script_code: 'Jpan', name:,
tag_id: tag.id, language_code: 'ain', script_code: 'Kana', name:,
primary_flg: true, auto_generated: false,
created_at: Time.current, updated_at: Time.current }])
tag.tag_names.reset
+10 -15
ファイルの表示
@@ -1,22 +1,17 @@
RSpec.shared_context 'English locale' do
def prepare_english_reference!
Language.find_or_create_by!(code: 'en') { _1.name = 'English' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
region_class = Class.new(ApplicationRecord) { self.table_name = 'regions' }
region_class.find_or_create_by!(code: 826) do |record|
record.alpha_code = 'GB'
record.name = 'United Kingdom'
end
RSpec.shared_context 'Ainu locale' do
def prepare_ainu_reference!
Language.find_or_create_by!(code: 'ain') { _1.name = 'ain' }
Script.find_or_create_by!(code: 'Kana') { _1.name = 'Kana' }
end
let(:locale) do
prepare_english_reference!
Locale.find_or_create_by!(code: 'en') do |record|
record.language_code = 'en'
record.script_code = 'Latn'
record.region_code = 826
prepare_ainu_reference!
Locale.find_or_create_by!(code: 'ain-Kana') do |record|
record.language_code = 'ain'
record.script_code = 'Kana'
record.region_code = 392
record.variants = ''
record.name = 'English'
record.name = 'Ainu'
end
end
end
+54 -33
ファイルの表示
@@ -5,7 +5,8 @@ import { createBrowserRouter,
Route,
RouterProvider,
Routes,
useLocation } from 'react-router-dom'
useLocation,
useParams } from 'react-router-dom'
import DevModeWatermark from '@/components/DevModeWatermark'
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
@@ -13,6 +14,8 @@ import TopNav from '@/components/TopNav'
import DialogueProvider from '@/components/dialogues/DialogueProvider'
import { Toaster } from '@/components/ui/toaster'
import { apiPost, isApiError } from '@/lib/api'
import { isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
fetchUserThemeSlots,
@@ -73,41 +76,41 @@ const RouteTransitionWrapper = ({ animationMode, user, setUser }: {
const routes = (
<Routes location={location}>
<Route path="/" element={<Navigate to="/posts" replace/>}/>
<Route path="/posts" element={<PostListPage/>}/>
<Route path="/posts/new" element={<PostNewPage user={user}/>}/>
<Route path="/posts/search" element={<PostSearchPage/>}/>
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
<Route path="/tags" element={<TagListPage/>}/>
<Route path="/tags/:id" element={<TagDetailPage/>}/>
<Route path="/tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="/nico/tags" element={<NicoTagListPage user={user}/>}/>
<Route path="/tags/changes" element={<TagHistoryPage/>}/>
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
<Route path="/materials" element={<MaterialBasePage/>}>
<Route path="" element={<LocaleRedirect to="/posts"/>}/>
<Route path="posts" element={<PostListPage/>}/>
<Route path="posts/new" element={<PostNewPage user={user}/>}/>
<Route path="posts/search" element={<PostSearchPage/>}/>
<Route path="posts/:id" element={<PostDetailRoute user={user}/>}/>
<Route path="posts/changes" element={<PostHistoryPage/>}/>
<Route path="tags" element={<TagListPage/>}/>
<Route path="tags/:id" element={<TagDetailPage/>}/>
<Route path="tags/:id/deerjikists" element={<DeerjikistDetailPage/>}/>
<Route path="tags/nico" element={<NicoTagListPage user={user}/>}/>
<Route path="nico/tags" element={<NicoTagListPage user={user}/>}/>
<Route path="tags/changes" element={<TagHistoryPage/>}/>
<Route path="theatres/:id" element={<TheatreDetailPage user={user}/>}/>
<Route path="materials" element={<MaterialBasePage/>}>
<Route index element={<MaterialListPage/>}/>
<Route path="changes" element={<MaterialHistoryPage/>}/>
<Route path="new" element={<MaterialNewPage/>}/>
<Route path="suppressions" element={<MaterialSyncSuppressionsPage/>}/>
<Route path=":id" element ={<MaterialDetailPage/>}/>
</Route>
<Route path="/wiki" element={<WikiSearchPage/>}/>
<Route path="/wiki/:title" element={<WikiDetailPage/>}/>
<Route path="/wiki/new" element={<WikiNewPage user={user}/>}/>
<Route path="/wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
<Route path="/wiki/:id/diff" element={<WikiDiffPage/>}/>
<Route path="/wiki/changes" element={<WikiHistoryPage/>}/>
<Route path="/help" element={<HelpListPage/>}/>
<Route path="/help/changes" element={<HelpChangesPage/>}/>
<Route path="/help/:identifier/diff" element={<HelpDiffPage/>}/>
<Route path="/help/:identifier" element={<HelpDetailPage/>}/>
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
<Route path="/settings" element={<Navigate to="/users/settings" replace/>}/>
<Route path="/tos" element={<TOSPage/>}/>
<Route path="/gekanator" element={<GekanatorPage user={user}/>}/>
<Route path="/more" element={<MorePage/>}/>
<Route path="wiki" element={<WikiSearchPage/>}/>
<Route path="wiki/:title" element={<WikiDetailPage/>}/>
<Route path="wiki/new" element={<WikiNewPage user={user}/>}/>
<Route path="wiki/:id/edit" element={<WikiEditPage user={user}/>}/>
<Route path="wiki/:id/diff" element={<WikiDiffPage/>}/>
<Route path="wiki/changes" element={<WikiHistoryPage/>}/>
<Route path="help" element={<HelpListPage/>}/>
<Route path="help/changes" element={<HelpChangesPage/>}/>
<Route path="help/:identifier/diff" element={<HelpDiffPage/>}/>
<Route path="help/:identifier" element={<HelpDetailPage/>}/>
<Route path="users/settings" element={<SettingPage user={user} setUser={setUser}/>}/>
<Route path="settings" element={<LocaleRedirect to="/users/settings"/>}/>
<Route path="tos" element={<TOSPage/>}/>
<Route path="gekanator" element={<GekanatorPage user={user}/>}/>
<Route path="more" element={<MorePage/>}/>
<Route path="*" element={<NotFound/>}/>
</Routes>)
@@ -259,9 +262,27 @@ const RoutedApp: FC = () => {
}
const router = createBrowserRouter ([{
path: '*',
element: <RoutedApp/> }])
const LocaleRoute: FC = () => {
const { locale } = useParams ()
const location = useLocation ()
if (isLocaleCode (locale))
return <RoutedApp/>
const redirect = legacyLocalePath (location)
return redirect == null ? <NotFound/> : <Navigate to={redirect} replace/>
}
const LocaleRedirect = ({ to }: { to: string }) => {
const locale = useLocale ()
const location = useLocation ()
return <Navigate to={switchLocalePath ({ ...location, pathname: to }, locale)} replace/>
}
const router = createBrowserRouter ([
{ path: '/:locale/*', element: <LocaleRoute/> },
{ path: '*', element: <LocaleRoute/> }])
const App: FC = () => (
+13 -9
ファイルの表示
@@ -1,7 +1,8 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import SidebarComponent from '@/components/layout/SidebarComponent'
import TagLink from '@/components/TagLink'
import { fetchMaterialTagTree, parseMaterialFilter } from '@/lib/materials'
@@ -83,7 +84,7 @@ const tagSelectionShellClass = (selected: boolean): string =>
const updateMaterialFilterQuery = (
pathname: string,
locationSearch: string,
navigate: ReturnType<typeof useNavigate>,
navigate: ReturnType<typeof useLocaleNavigate>,
materialFilter: MaterialFilter,
) => {
const qs = new URLSearchParams (locationSearch)
@@ -125,10 +126,11 @@ const MaterialTreeNode: FC<{
tag: MaterialSidebarTag
}> = ({ materialFilter, nestLevel = 0, onChildren, openTags, selectedTagId,
setOpenTags, tag }) => {
const locale = useLocale ()
const open = Boolean (openTags[tag.id])
const { data } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }, locale),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }, locale),
enabled: open && tag.hasChildren && tag.children.length === 0})
useEffect (() => {
@@ -200,6 +202,7 @@ const MobileMaterialTreeNode: FC<{ depth?: number
tag,
},
) => {
const locale = useLocale ()
const open = Boolean (openTags[tag.id])
const tagColumnRef = useRef<HTMLDivElement | null> (null)
const chipRef = useRef<HTMLDivElement | null> (null)
@@ -210,8 +213,8 @@ const MobileMaterialTreeNode: FC<{ depth?: number
const [tagLinkInlineSizePx, setTagLinkInlineSizePx] = useState<number | null> (null)
const [childAvailableInlineSizePx, setChildAvailableInlineSizePx] = useState<number | null> (null)
const { data } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }, locale),
queryFn: () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }, locale),
enabled: open && tag.hasChildren && tag.children.length === 0})
useEffect (() => {
@@ -382,8 +385,9 @@ const MobileMaterialTreeNode: FC<{ depth?: number
const MaterialSidebar: FC = () => {
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const qs = new URLSearchParams (location.search)
const materialFilter = parseMaterialFilter (qs.get ('material_filter'), 'any')
const selectedTagId = Number (qs.get ('tag_id') ?? 0) || null
@@ -395,8 +399,8 @@ const MaterialSidebar: FC = () => {
useState<number | null> (null)
const { data: rootTags = [], isLoading, isError } = useQuery ({
queryKey: materialsKeys.tree ({ parentId: null, materialFilter }),
queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter })})
queryKey: materialsKeys.tree ({ parentId: null, materialFilter }, locale),
queryFn: () => fetchMaterialTagTree ({ parentId: null, materialFilter }, locale)})
useEffect (() => {
setDesktopTags (rootTags)
+3 -1
ファイルの表示
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
import FieldError from '@/components/common/FieldError'
import PostDurationField from '@/components/posts/PostDurationField'
@@ -51,6 +52,7 @@ type Props = { post: Post
const PostEditForm: FC<Props> = ({ post, onSave }) => {
const locale = useLocale ()
const [disabled, setDisabled] = useState (false)
const [duration, setDuration] = useState<string> (videoMsToDurationValue (post.videoMs))
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
@@ -75,7 +77,7 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
try
{
const data = await updatePost (...args)
const data = await updatePost (...args, undefined, locale)
onSave ({ ...post,
versionNo: data.versionNo,
title: data.title,
+3 -1
ファイルの表示
@@ -2,6 +2,7 @@
import { useRef, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import TagSearchBox from '@/components/TagSearchBox'
import FormField from '@/components/common/FormField'
import TextArea from '@/components/common/TextArea'
@@ -38,6 +39,7 @@ type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | '
const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
const locale = useLocale ()
const ref = useRef<HTMLTextAreaElement> (null)
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
@@ -68,7 +70,7 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
setBounds ({ start, end })
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: {
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: {
q: token,
nico: '0' } })
const nextSuggestions = data.filter (t => t.postCount > 0)
+11 -5
ファイルの表示
@@ -1,8 +1,10 @@
import { useQueryClient } from '@tanstack/react-query'
import { forwardRef, useMemo } from 'react'
import { flushSync } from 'react-dom'
import { createPath, useNavigate } from 'react-router-dom'
import { createPath } from 'react-router-dom'
import { localiseTo } from '@/lib/locales'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
@@ -32,13 +34,17 @@ export default forwardRef<HTMLAnchorElement, Props> (({
if ('onClick' in rest)
delete rest['onClick']
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const locale = useLocale ()
const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings ()
const linkPreloadMode = behaviourSettings.linkPreload ?? 'intent'
const path = useMemo (
() => typeof to === 'string' ? to : createPath (to),
[to],
() => {
const localised = localiseTo (to, locale)
return typeof localised === 'string' ? localised : createPath (localised)
},
[to, locale],
)
const url = useMemo (() => {
return (new URL (path, window.location.origin)).toString ()
@@ -98,7 +104,7 @@ export default forwardRef<HTMLAnchorElement, Props> (({
if (!(ok) && cancelOnError)
return
navigate (to, { replace, ...(state && { state }) })
navigate (path, { replace, ...(state && { state }) })
}
catch (ex)
{
+12 -7
ファイルの表示
@@ -12,6 +12,7 @@ import { useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
@@ -27,6 +28,7 @@ import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, originalCreatedAtString } from '@/lib/utils'
import type { LocaleCode } from '@/lib/locales'
import type { CollisionDetection, DragEndEvent } from '@dnd-kit/core'
import type { FC, MutableRefObject, ReactNode } from 'react'
@@ -180,8 +182,10 @@ const buildFlatTagByCategory = (
const changeCategory = async (
tagId: number,
category: Category): Promise<void> => {
await apiPatch (`/tags/${ tagId }`, { category })
category: Category,
locale: LocaleCode = 'ja',
): Promise<void> => {
await apiPatch (`/tags/${ tagId }`, { category }, { locale })
}
@@ -227,6 +231,7 @@ type ActiveTagDrag = {
const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
const locale = useLocale ()
sp = Boolean (sp)
const qc = useQueryClient ()
@@ -271,7 +276,7 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }))
const reloadTags = async (): Promise<void> => {
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`)))
setTags (buildTagByCategory (await apiGet<Post> (`/posts/${ post.id }`, { locale })))
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
}
@@ -314,9 +319,9 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
return
if (fromParentId != null)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`, { locale })
await apiPost (`/tags/${ parentId }/children/${ childId }`, { })
await apiPost (`/tags/${ parentId }/children/${ childId }`, { }, { locale })
await reloadTags ()
toast ({
@@ -333,10 +338,10 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
return
if (child.category !== cat)
await changeCategory (childId, cat)
await changeCategory (childId, cat, locale)
if (fromParentId != null)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`)
await apiDelete (`/tags/${ fromParentId }/children/${ childId }`, { locale })
const fromParent = fromParentId == null ? null : findTag (tags, fromParentId)
+5 -3
ファイルの表示
@@ -1,8 +1,9 @@
// TODO: タグ入力系すべてに同様の処理あるため共通化する.
import { useEffect, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { apiGet } from '@/lib/api'
import { inputClass } from '@/lib/utils'
@@ -14,8 +15,9 @@ import type { Tag } from '@/types'
const TagSearch: FC = () => {
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [activeIndex, setActiveIndex] = useState (-1)
const [search, setSearch] = useState ('')
@@ -32,7 +34,7 @@ const TagSearch: FC = () => {
return
}
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q } })
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: { q } })
setSuggestions (data.filter (t => t.postCount > 0))
if (suggestions.length > 0)
setSuggestionsVsbl (true)
+5 -3
ファイルの表示
@@ -1,7 +1,8 @@
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle'
@@ -22,7 +23,8 @@ type Props = { posts: Post[]
const TagSidebar: FC<Props> = ({ posts, onClick }) => {
const navigate = useNavigate ()
const locale = useLocale ()
const navigate = useLocaleNavigate ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const animationsOff = animationMode === 'off'
@@ -96,7 +98,7 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
try
{
const data = await apiGet<Post> ('/posts/random',
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
{ locale, params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
match: (anyFlg ? 'any' : 'all') } })
navigate (`/posts/${ data.id }`)
}
+82 -52
ファイルの表示
@@ -3,10 +3,18 @@ import { AnimatePresence, motion } from 'framer-motion'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
import TopNavUser from '@/components/TopNavUser'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import { LOCALE_LABELS,
SUPPORTED_LOCALES,
isLocaleCode,
localeFromPath,
switchLocalePath,
unlocalisedPath } from '@/lib/locales'
import { uiMessage, uiMessageLanguage } from '@/lib/messages'
import { materialsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users'
@@ -24,13 +32,16 @@ type NavDensity = 'auto' | 'comfortable' | 'compact'
export const menuOutline = (
{ tag, material, wikiId, user, pathName }: {
{ tag, material, wikiId, user, pathName: fullPathname }: {
tag?: Tag | null
material?: Material | null
wikiId: number | null
user: User | null
pathName: string },
): Menu => {
const pathName = unlocalisedPath (fullPathname)
const locale = localeFromPath (fullPathname) ?? 'ja'
const message = (key: Parameters<typeof uiMessage>[1]) => uiMessage (locale, key)
const postCount = tag?.postCount ?? material?.tag?.postCount ?? 0
const editable = canEditContent (user)
@@ -41,71 +52,73 @@ export const menuOutline = (
const materialFlg = /^\/materials\/\d+/.test (pathName)
const helpVsbl = /\/help\/((?!changes)[^\/?]+)/.test (pathName)
const helpVsbl = /\/help\/((?!changes)[^/?]+)/.test (pathName)
const helpIdentifier = pathName.split ('/')[2] ?? ''
return [
{ name: '広場', to: '/posts', subMenu: [
{ name: '一覧', to: '/posts' },
{ name: '検索', to: '/posts/search' },
{ name: '追加', to: '/posts/new', visible: editable },
{ name: '全体履歴', to: '/posts/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
{ name: 'タグ', to: '/tags', subMenu: [
{ name: 'マスタ', to: '/tags' },
{ name: 'ニコニコ連携', to: '/tags/nico' },
{ name: '全体履歴', to: '/tags/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
{ name: message ('square'), to: '/posts', subMenu: [
{ name: message ('list'), to: '/posts' },
{ name: message ('search'), to: '/posts/search' },
{ name: message ('add'), to: '/posts/new', visible: editable },
{ name: message ('allHistory'), to: '/posts/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:広場' }] },
{ name: message ('tags'), to: '/tags', subMenu: [
{ name: message ('master'), to: '/tags' },
{ name: message ('nicoLinks'), to: '/tags/nico' },
{ name: message ('allHistory'), to: '/tags/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:タグ' },
{ component: <Separator/>, visible: tagFlg },
{ name: `広場 (${ postCount || 0 })`,
to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`,
visible: tagFlg },
{ name: '履歴', to: `/tags/changes?id=${ tag?.id }`,
{ name: message ('history'), to: `/tags/changes?id=${ tag?.id }`,
visible: tagFlg && tag?.category !== 'nico' }] },
{ name: '素材', to: '/materials', visible: true, subMenu: [
{ name: '一覧', to: '/materials' },
{ name: '追加', to: '/materials/new', visible: editable },
{ name: '抑止', to: '/materials/suppressions',
{ name: message ('materials'), to: '/materials', visible: true, subMenu: [
{ name: message ('list'), to: '/materials' },
{ name: message ('add'), to: '/materials/new', visible: editable },
{ name: message ('suppressions'), to: '/materials/suppressions',
visible: user?.role === 'admin' },
{ name: '全体履歴', to: '/materials/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:素材管理' },
{ name: message ('allHistory'), to: '/materials/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:素材管理' },
{ component: <Separator/>, visible: materialFlg },
{ name: `広場 (${ postCount || 0 })`,
to: `/posts?tags=${ encodeURIComponent (material?.tag?.name ?? '') }`,
visible: materialFlg && Boolean (material?.tag) },
{ name: '履歴', to: `/materials/changes?material_id=${ material?.id }`,
{ name: message ('history'), to: `/materials/changes?material_id=${ material?.id }`,
visible: materialFlg }] },
{ name: 'Wiki', to: '/wiki', subMenu: [
{ name: '検索', to: '/wiki' },
{ name: '新規', to: '/wiki/new', visible: editable },
{ name: '全体履歴', to: '/wiki/changes' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' },
{ name: message ('wiki'), to: '/wiki', subMenu: [
{ name: message ('search'), to: '/wiki' },
{ name: message ('create'), to: '/wiki/new', visible: editable },
{ name: message ('allHistory'), to: '/wiki/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:Wiki' },
{ component: <Separator/>, visible: wikiPageFlg },
{ name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
visible: wikiPageFlg },
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`,
{ name: message ('history'), to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ name: message ('edit'), to: `/wiki/${ wikiId || wikiTitle }/edit`,
visible: wikiPageFlg && editable }] },
{ name: 'ヘルプ', to: '/help/home', base: '/help', subMenu: [
{ name: '検索', to: '/help' },
{ name: '全体履歴', to: '/help/changes' },
{ name: message ('help'), to: '/help/home', base: '/help', subMenu: [
{ name: message ('search'), to: '/help' },
{ name: message ('allHistory'), to: '/help/changes' },
{ component: <Separator/>, visible: helpVsbl },
{ name: '履歴',
{ name: message ('history'),
to: `/help/changes?identifier=${ helpIdentifier }`,
visible: helpVsbl }] },
{ name: 'おたのしみ', visible: false, subMenu: [
{ name: '上映会 (β)', to: '/theatres/1' },
{ name: 'グカネータ (β)', to: '/gekanator' }] },
{ name: 'ユーザ', to: '/users/settings', visible: false, subMenu: [
{ name: '一覧', to: '/users', visible: false },
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] },
{ name: '法規', visible: false, subMenu: [
{ name: '利用規約', to: '/tos' }] }]
{ name: message ('entertainment'), visible: false, subMenu: [
{ name: message ('theatre'), to: '/theatres/1' },
{ name: message ('gekanator'), to: '/gekanator' }] },
{ name: message ('users'), to: '/users/settings', visible: false, subMenu: [
{ name: message ('list'), to: '/users', visible: false },
{ name: message ('currentUser'), to: `/users/${ user?.id }`, visible: false },
{ name: message ('settings'), to: '/users/settings', visible: Boolean (user) }] },
{ name: message ('legal'), visible: false, subMenu: [
{ name: message ('terms'), to: '/tos' }] }]
}
const TopNav: FC<Props> = ({ user }) => {
const locale = useLocale ()
const navigate = useLocaleNavigate ()
const location = useLocation ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
@@ -151,25 +164,26 @@ const TopNav: FC<Props> = ({ user }) => {
const { data: wikiPage } = useQuery ({
enabled: Boolean (wikiIdStr),
queryKey: wikiKeys.show (wikiIdStr, { }),
queryFn: () => fetchWikiPage (wikiIdStr, { }) })
queryKey: wikiKeys.show (wikiIdStr, { }, locale),
queryFn: () => fetchWikiPage (wikiIdStr, { }, locale) })
const tagFlg = /^\/tags\/\d+/.test (location.pathname)
const materialFlg = /^\/materials\/\d+/.test (location.pathname)
const pathname = unlocalisedPath (location.pathname)
const tagFlg = /^\/tags\/\d+/.test (pathname)
const materialFlg = /^\/materials\/\d+/.test (pathname)
const effectiveTitle = (((tagFlg || materialFlg)
? location.pathname.split ('/')[2]
? pathname.split ('/')[2]
: wikiPage?.title)
?? '')
const { data: tag } = useQuery ({
enabled: Boolean (effectiveTitle),
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle) })
queryKey: tagsKeys.show (effectiveTitle, locale),
queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle, locale) })
const { data: material } = useQuery ({
enabled: Boolean (effectiveTitle),
queryKey: materialsKeys.show (effectiveTitle),
queryFn: () => fetchMaterial (effectiveTitle) })
queryKey: materialsKeys.show (effectiveTitle, locale),
queryFn: () => fetchMaterial (effectiveTitle, locale) })
const menu = menuOutline ({ tag, material, wikiId, user, pathName: location.pathname })
const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
@@ -177,7 +191,7 @@ const TopNav: FC<Props> = ({ user }) => {
!(item.visible ?? true)
|| item.subMenu.filter (subItem => subItem.visible ?? true).length > 0)
const activeIdx =
visibleMenu.findIndex (item => location.pathname.startsWith (item.base || item.to))
visibleMenu.findIndex (item => pathname.startsWith (item.base || item.to))
const submenuHeight = moreVsbl ? 40 * moreMenu.length : (activeIdx < 0 ? 0 : 40)
const topNavTransition =
animationsOff
@@ -298,7 +312,7 @@ const TopNav: FC<Props> = ({ user }) => {
return (
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
data-desktop={desktop} data-density={density}>
<nav ref={barRef}
<nav ref={barRef} lang={uiMessageLanguage (locale, 'square')}
className="top-nav-root top-nav-bar flex justify-between items-center w-full">
<div className="top-nav-primary flex shrink-0 items-center h-12">
<PrefetchLink
@@ -373,6 +387,22 @@ const TopNav: FC<Props> = ({ user }) => {
</button>
</nav>
<div className="flex min-w-0 justify-end px-2">
<select
aria-label={LOCALE_LABELS[locale]}
value={locale}
className="max-w-full rounded border border-gray-300 bg-white text-xs text-gray-900
dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
onChange={event => {
const nextLocale = event.target.value
if (isLocaleCode (nextLocale))
navigate (switchLocalePath (location, nextLocale))
}}>
{SUPPORTED_LOCALES.map (code => (
<option key={code} value={code}>{LOCALE_LABELS[code]}</option>))}
</select>
</div>
<AnimatePresence initial={false}>
<motion.div
key="submenu-shell"
+4 -2
ファイルの表示
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGFM from 'remark-gfm'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import { wikiKeys } from '@/lib/queryKeys'
import remarkWikiAutoLink from '@/lib/remark-wiki-autolink'
@@ -26,10 +27,11 @@ const mdComponents = { a: (({ href, children }) => (
const WikiBody: FC<Props> = ({ title, body }) => {
const locale = useLocale ()
const { data } = useQuery ({
enabled: Boolean (body),
queryKey: wikiKeys.index ({ }),
queryFn: () => fetchWikiPages ({ }) })
queryKey: wikiKeys.index ({ }, locale),
queryFn: () => fetchWikiPages ({ }, locale) })
const pageNames = (data ?? []).map (page => page.title).sort ((a, b) => b.length - a.length)
const remarkPlugins = useMemo (
+3 -1
ファイルの表示
@@ -1,5 +1,6 @@
import { useState } from 'react'
import { useLocale } from '@/lib/useLocale'
import TagSearchBox from '@/components/TagSearchBox'
import { apiGet } from '@/lib/api'
import { inputClass } from '@/lib/utils'
@@ -24,6 +25,7 @@ const TagInput: FC<Props> = ({
value,
setValue,
}) => {
const locale = useLocale ()
const [activeIndex, setActiveIndex] = useState (-1)
const [suggestions, setSuggestions] = useState<Tag[]> ([])
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
@@ -39,7 +41,7 @@ const TagInput: FC<Props> = ({
return
}
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: {
const data = await apiGet<Tag[]> ('/tags/autocomplete', { locale, params: {
q,
nico: includeNico ? '1' : '0' } })
const nextSuggestions = data.filter (t => t.postCount > 0)
+3 -2
ファイルの表示
@@ -88,8 +88,9 @@ const DialogueProvider: FC<Props> = ({ children }) => {
})
setPendingIds (current => current.filter (pendingId => pendingId !== id))
setFormActions (current => {
const { [id]: _, ...rest } = current
return rest
const rest = { ...current }
delete rest[id]
return rest
})
delete formControls.current[id]
}, [])
+11 -4
ファイルの表示
@@ -5,7 +5,10 @@ import { API_BASE_URL } from '@/config'
import type { AxiosError, AxiosRequestConfig } from 'axios'
import type { LocaleCode } from '@/lib/locales'
type Opt = {
locale?: LocaleCode
params?: AxiosRequestConfig['params']
headers?: Record<string, string>
signal?: AbortSignal
@@ -14,10 +17,14 @@ type Opt = {
const client = axios.create ({ baseURL: API_BASE_URL })
const withUserCode = (opt?: Opt): Opt => ({
...opt,
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '',
...(opt?.headers ?? { }) } })
const withUserCode = (opt?: Opt): AxiosRequestConfig => {
const { locale, ...config } = opt ?? { }
return {
...config,
...(locale != null && { params: { locale, ...config.params } }),
headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '',
...(config.headers ?? { }) } }
}
const apiP = async <T> (
+31 -15
ファイルの表示
@@ -1,5 +1,6 @@
import { apiGet, apiPost } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { Post } from '@/types'
export type GekanatorAnswerValue =
@@ -411,24 +412,30 @@ export const storeGekanatorQuestion = (
exampleAnswers: question.exampleAnswers })
export const fetchGekanatorPosts = async (): Promise<Post[]> => {
const data = await apiGet<{ posts: Post[] }> ('/gekanator/posts')
export const fetchGekanatorPosts = async (
locale: LocaleCode = 'ja',
): Promise<Post[]> => {
const data = await apiGet<{ posts: Post[] }> ('/gekanator/posts', { locale })
return data.posts
}
export const fetchGekanatorQuestions = async (): Promise<StoredGekanatorQuestion[]> => {
const data = await apiGet<{ questions: StoredGekanatorQuestion[] }> ('/gekanator/questions')
export const fetchGekanatorQuestions = async (
locale: LocaleCode = 'ja',
): Promise<StoredGekanatorQuestion[]> => {
const data = await apiGet<{ questions: StoredGekanatorQuestion[] }> ('/gekanator/questions', { locale })
return data.questions
}
export const fetchGekanatorExtraQuestions = async (
gameId: number,
nonce?: string): Promise<GekanatorExtraQuestion[]> => {
nonce?: string,
locale: LocaleCode = 'ja',
): Promise<GekanatorExtraQuestion[]> => {
const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> (
`/gekanator/games/${ gameId }/extra_questions`,
{ params: nonce ? { nonce } : undefined })
{ locale, params: nonce ? { nonce } : undefined })
return data.questions
}
@@ -606,7 +613,8 @@ export const buildGekanatorQuestions = (
}
export const saveGekanatorGame = async ({
export const saveGekanatorGame = async (
{
guessedPostId,
correctPostId,
answers,
@@ -614,7 +622,9 @@ export const saveGekanatorGame = async ({
guessedPostId: number
correctPostId: number
answers: GekanatorAnswerLog[]
}): Promise<{ id: number; learnedExampleCount: number }> =>
},
locale: LocaleCode = 'ja',
): Promise<{ id: number; learnedExampleCount: number }> =>
await apiPost ('/gekanator/games', {
guessed_post_id: guessedPostId,
correct_post_id: correctPostId,
@@ -627,10 +637,11 @@ export const saveGekanatorGame = async ({
effective_question: answer.effectiveQuestion,
learning_question: answer.learningQuestion,
answer: answer.answer,
original_answer: answer.originalAnswer })) })
original_answer: answer.originalAnswer })) }, { locale })
export const saveGekanatorQuestionSuggestion = async ({
export const saveGekanatorQuestionSuggestion = async (
{
gekanatorGameId,
existingQuestionId,
questionText,
@@ -640,22 +651,27 @@ export const saveGekanatorQuestionSuggestion = async ({
existingQuestionId?: number
questionText?: string
answer: GekanatorAnswerValue
}): Promise<{ id: number; count: number }> =>
},
locale: LocaleCode = 'ja',
): Promise<{ id: number; count: number }> =>
await apiPost ('/gekanator/question_suggestions', {
gekanator_game_id: gekanatorGameId,
existing_question_id: existingQuestionId,
question_text: questionText,
answer })
answer }, { locale })
export const saveGekanatorExtraQuestionAnswers = async ({
export const saveGekanatorExtraQuestionAnswers = async (
{
gameId,
answers,
}: {
gameId: number
answers: { questionId: number; answer: GekanatorAnswerValue }[]
}) =>
},
locale: LocaleCode = 'ja',
) =>
await apiPost (`/gekanator/games/${ gameId }/extra_question_answers`, {
answers: answers.map (item => ({
question_id: item.questionId,
answer: item.answer })) })
answer: item.answer })) }, { locale })
+16 -3
ファイルの表示
@@ -13,12 +13,14 @@ export type HelpEditionText = {
export type HelpSearchEntry = HelpEditionText & {
identifier: string
language?: string
edition: string }
export type HelpEditionContent = HelpEditionText & { content: ComponentType }
export type HelpEditionSource = HelpEditionText & { source: string }
export type HelpChange = Pick<HelpSearchEntry, 'identifier' | 'edition' | 'title'> & {
language?: string
pred: string | null
change: 'new' | 'update'
date: string }
@@ -82,6 +84,15 @@ export const getHelpArticle = async (identifier: string): Promise<ArticleMeta |
}
export const resolveHelpLanguage = (
article: ArticleMeta | null | undefined,
requestedLanguage: string,
): string =>
article?.languages[requestedLanguage] != null
? requestedLanguage
: article?.canonicalLanguage ?? 'ja'
export const getHelpEditions = async (
identifier: string,
language: string,
@@ -144,7 +155,8 @@ export const getHelpSearchEntries = async (
language: string,
): Promise<HelpSearchEntry[]> => {
const { default: search } = await import ('virtual:help/search')
return search[language] ?? []
const contentLanguage = search[language] != null ? language : 'ja'
return (search[contentLanguage] ?? []).map (entry => ({ ...entry, language: contentLanguage }))
}
@@ -163,13 +175,14 @@ export const getHelpEditionContent = async (
export const getHelpChanges = async (language: string): Promise<HelpChange[]> => {
const { default: changes } = await import ('virtual:help/changes')
return changes[language] ?? []
const contentLanguage = changes[language] != null ? language : 'ja'
return (changes[contentLanguage] ?? []).map (entry => ({ ...entry, language: contentLanguage }))
}
export const getHelpTitleIndex = async (language: string): Promise<HelpTitleIndex> => {
const { default: titles } = await import ('virtual:help/titles')
return titles[language] ?? { }
return titles[language] ?? titles.ja ?? { }
}
+4 -1
ファイルの表示
@@ -1,3 +1,5 @@
import { unlocalisedPath } from '@/lib/locales'
export type ShortcutScope =
| 'global'
| 'posts'
@@ -308,7 +310,8 @@ export const getConflictingShortcutActionIds = (
)
}
export const resolveShortcutScope = (pathname: string): ShortcutScope => {
export const resolveShortcutScope = (fullPathname: string): ShortcutScope => {
const pathname = unlocalisedPath (fullPathname)
if (pathname.startsWith ('/posts/search'))
return 'postSearch'
if (pathname.startsWith ('/posts'))
+55
ファイルの表示
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { isLocaleCode,
legacyLocalePath,
localeFromPath,
localiseTo,
switchLocalePath,
unlocalisedPath } from '@/lib/locales'
describe ('locale URL generation', () => {
it.each (['ja', 'ain-Kana']) ('recognises the supported locale %s', locale => {
expect (isLocaleCode (locale)).toBe (true)
expect (localeFromPath (`/${ locale }/posts`)).toBe (locale)
})
it.each (['en', 'ain', 'JA', '', undefined]) ('rejects %s', locale => {
expect (isLocaleCode (locale)).toBe (false)
})
it.each ([
'/posts', '/posts/new', '/posts/search', '/posts/42', '/posts/changes',
'/tags', '/tags/42', '/tags/42/deerjikists', '/tags/nico', '/nico/tags', '/tags/changes',
'/materials', '/materials/new', '/materials/42', '/materials/changes',
'/materials/suppressions', '/theatres/42', '/wiki', '/wiki/new', '/wiki/title',
'/wiki/42/edit', '/wiki/42/diff', '/wiki/changes', '/help', '/help/changes',
'/help/article', '/help/article/diff', '/users/settings', '/settings',
'/tos', '/gekanator', '/more']) ('localises and redirects %s', pathname => {
const location = { pathname, search: '?page=2&tag=a%23b', hash: '#section' }
expect (localiseTo (pathname, 'ain-Kana')).toBe (`/ain-Kana${ pathname }`)
expect (legacyLocalePath (location)).toBe (`/ja${ pathname }${ location.search }#section`)
expect (unlocalisedPath (`/ain-Kana${ pathname }`)).toBe (pathname)
})
it ('preserves the resource, query and fragment on locale changes', () => {
expect (switchLocalePath ({ pathname: '/ja/tags/42', search: '?x=1&x=2', hash: '#history' },
'ain-Kana')).toBe ('/ain-Kana/tags/42?x=1&x=2#history')
})
it ('preserves explicit locales and relative, fragment and external targets', () => {
for (const target of ['/ja/posts', '/ain-Kana/posts', '?page=2', '#body',
'../changes', 'https://example.com/posts', '//example.com/posts'])
expect (localiseTo (target, 'ain-Kana')).toBe (target)
})
it ('preserves structured targets', () => {
expect (localiseTo ({ pathname: '/tags/42', search: '?q=one', hash: '#two' }, 'ain-Kana'))
.toEqual ({ pathname: '/ain-Kana/tags/42', search: '?q=one', hash: '#two' })
})
it.each (['/en/posts', '/ain/posts', '/unknown', '/api/tags', '/assets/image.png']) (
'does not redirect invalid or non-page paths: %s', pathname => {
expect (legacyLocalePath ({ pathname, search: '', hash: '' })).toBeNull ()
})
})
+62
ファイルの表示
@@ -0,0 +1,62 @@
import type { To } from 'react-router-dom'
export const SUPPORTED_LOCALES = ['ja', 'ain-Kana'] as const
export type LocaleCode = typeof SUPPORTED_LOCALES[number]
export const LOCALE_LABELS: Record<LocaleCode, string> = {
ja: '日本語', 'ain-Kana': 'ヤウンモシルン アイヌ イタㇰ' }
const LEGACY_ROOTS = [
'posts', 'tags', 'nico', 'theatres', 'materials', 'wiki', 'help',
'users', 'settings', 'tos', 'gekanator', 'more']
export const isLocaleCode = (value: unknown): value is LocaleCode =>
SUPPORTED_LOCALES.some (locale => locale === value)
export const localeLanguage = (locale: LocaleCode): 'ja' | 'ain' =>
locale === 'ja' ? 'ja' : 'ain'
export const localeFromPath = (pathname: string): LocaleCode | null => {
const segment = pathname.split (/[/?#]/)[1]
return isLocaleCode (segment) ? segment : null
}
export const unlocalisedPath = (pathname: string): string => {
const locale = localeFromPath (pathname)
return locale == null ? pathname : pathname.slice (locale.length + 1) || '/'
}
export const localiseTo = (to: To, locale: LocaleCode): To => {
const pathname = typeof to === 'string' ? to : to.pathname
if (pathname == null
|| !(pathname.startsWith ('/'))
|| pathname.startsWith ('//')
|| localeFromPath (pathname) != null)
return to
const localised = `/${ locale }${ pathname }`
return typeof to === 'string' ? localised : { ...to, pathname: localised }
}
export const switchLocalePath = (
location: { pathname: string; search: string; hash: string },
locale: LocaleCode,
): string =>
`/${ locale }${ unlocalisedPath (location.pathname) }${ location.search }${ location.hash }`
export const legacyLocalePath = (
location: { pathname: string; search: string; hash: string },
): string | null => {
const root = location.pathname.split ('/')[1]
return (location.pathname === '/' || LEGACY_ROOTS.includes (root))
? `/ja${ location.pathname }${ location.search }${ location.hash }`
: null
}
+38 -17
ファイルの表示
@@ -1,5 +1,6 @@
import { apiGet, isApiError, apiPost, apiPut } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { Material,
MaterialIndexResponse,
MaterialVersion,
@@ -36,8 +37,10 @@ export const fetchMaterials = async (
updatedFrom, updatedTo, sort, direction, page,
tagId, includeDescendants, groupBy,
limit }: FetchMaterialsParams,
locale: LocaleCode = 'ja',
): Promise<MaterialIndexResponse> =>
await apiGet ('/materials', { params: {
await apiGet ('/materials', { locale, params: {
...(q && { q }),
tag_state: tagState,
media_kind: mediaKind,
@@ -54,10 +57,13 @@ export const fetchMaterials = async (
limit} })
export const fetchMaterial = async (id: string): Promise<Material | null> => {
export const fetchMaterial = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Material | null> => {
try
{
return await apiGet (`/materials/${ id }`)
return await apiGet (`/materials/${ id }`, { locale })
}
catch (error)
{
@@ -75,8 +81,10 @@ export const fetchMaterialChanges = async (
eventType?: string
page: number
limit: number
}): Promise<MaterialChangesResponse> =>
await apiGet ('/materials/versions', { params: {
},
locale: LocaleCode = 'ja',
): Promise<MaterialChangesResponse> =>
await apiGet ('/materials/versions', { locale, params: {
...(materialId && { material_id: materialId }),
...(tag && { tag }),
...(eventType && { event_type: eventType }),
@@ -85,19 +93,23 @@ export const fetchMaterialChanges = async (
export const fetchMaterialTagTree = async (
{ parentId, materialFilter }: FetchMaterialTreeParams): Promise<MaterialSidebarTag[]> =>
await apiGet ('/tags/with-depth', { params: {
{ parentId, materialFilter }: FetchMaterialTreeParams,
locale: LocaleCode = 'ja',
): Promise<MaterialSidebarTag[]> =>
await apiGet ('/tags/with-depth', { locale, params: {
...(parentId != null && { parent: String (parentId) }),
material_filter: materialFilter} })
export const fetchMaterialTagByName = async (
name: string,
materialFilter: MaterialFilter): Promise<MaterialTagTree | null> => {
materialFilter: MaterialFilter,
locale: LocaleCode = 'ja',
): Promise<MaterialTagTree | null> => {
try
{
return await apiGet (`/tags/name/${ encodeURIComponent (name) }/materials`,
{ params: { material_filter: materialFilter } })
{ locale, params: { material_filter: materialFilter } })
}
catch (error)
{
@@ -108,19 +120,26 @@ export const fetchMaterialTagByName = async (
}
export const createMaterial = async (formData: FormData): Promise<Material> =>
await apiPost ('/materials', formData)
export const createMaterial = async (
formData: FormData,
locale: LocaleCode = 'ja',
): Promise<Material> =>
await apiPost ('/materials', formData, { locale })
export const updateMaterial = async (
id: string,
formData: FormData): Promise<Material> =>
await apiPut (`/materials/${ id }`, formData)
formData: FormData,
locale: LocaleCode = 'ja',
): Promise<Material> =>
await apiPut (`/materials/${ id }`, formData, { locale })
export const fetchMaterialSyncSuppressions =
async (): Promise<MaterialSyncSuppressionResponse> =>
await apiGet ('/materials/suppressions')
async (
locale: LocaleCode = 'ja',
): Promise<MaterialSyncSuppressionResponse> =>
await apiGet ('/materials/suppressions', { locale })
export const createMaterialSyncSuppression = async (
@@ -130,10 +149,12 @@ export const createMaterialSyncSuppression = async (
drivePath?: string
driveFileId?: string
reason: string
}): Promise<MaterialSyncSuppression> =>
},
locale: LocaleCode = 'ja',
): Promise<MaterialSyncSuppression> =>
await apiPost ('/materials/suppressions', {
source_kind: payload.sourceKind,
source_uri: payload.sourceUri,
drive_path: payload.drivePath,
drive_file_id: payload.driveFileId,
reason: payload.reason})
reason: payload.reason}, { locale })
+38
ファイルの表示
@@ -0,0 +1,38 @@
import type { LocaleCode } from '@/lib/locales'
const japanese = {
square: '広場',
list: '一覧',
search: '検索',
add: '追加',
history: '履歴',
allHistory: '全体履歴',
help: 'ヘルプ',
tags: 'タグ',
master: 'マスタ',
nicoLinks: 'ニコニコ連携',
materials: '素材',
suppressions: '抑止',
wiki: 'Wiki',
create: '新規',
edit: '編輯',
entertainment: 'おたのしみ',
theatre: '上映会 (β)',
gekanator: 'グカネータ (β)',
users: 'ユーザ',
currentUser: 'お前',
settings: '設定',
legal: '法規',
terms: '利用規約' }
export type UIMessageKey = keyof typeof japanese
const ainu: Partial<Record<UIMessageKey, string>> = { }
export const uiMessage = (locale: LocaleCode, key: UIMessageKey): string =>
(locale === 'ain-Kana' ? ainu[key] : null) ?? japanese[key]
export const uiMessageLanguage = (locale: LocaleCode, key: UIMessageKey): 'ja' | 'ain' =>
(locale === 'ain-Kana' && ainu[key] != null) ? 'ain' : 'ja'
+17 -7
ファイルの表示
@@ -1,14 +1,17 @@
import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { FetchPostsParams, Post, PostVersion } from '@/types'
export const fetchPosts = async (
{ url, title, tags, match, createdFrom, createdTo, updatedFrom, updatedTo,
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams): Promise<{
originalCreatedFrom, originalCreatedTo, page, limit, order }: FetchPostsParams,
locale: LocaleCode = 'ja',
): Promise<{
posts: Post[]
count: number }> =>
await apiGet ('/posts', { params: {
await apiGet ('/posts', { locale, params: {
...(url && { url }),
...(title && { title }),
...(tags && { tags }),
@@ -24,7 +27,10 @@ export const fetchPosts = async (
...(order && { order }) } })
export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/posts/${ id }`)
export const fetchPost = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Post> => await apiGet (`/posts/${ id }`, { locale })
export const fetchPostChanges = async (
@@ -33,10 +39,12 @@ export const fetchPostChanges = async (
tag?: string
externalTag?: string
page: number
limit: number }): Promise<{
limit: number },
locale: LocaleCode = 'ja',
): Promise<{
versions: PostVersion[]
count: number }> =>
await apiGet ('/posts/versions', { params: { ...(post && { post }),
await apiGet ('/posts/versions', { locale, params: { ...(post && { post }),
...(tag && { tag }),
...(externalTag && {
external_tag: externalTag }),
@@ -54,7 +62,9 @@ export const updatePost = async (
{ baseVersionNo, force, merge }: {
baseVersionNo?: number
force?: boolean
merge?: boolean }) =>
merge?: boolean },
locale: LocaleCode = 'ja',
) =>
await apiPut<Post> (
`/posts/${ post.id }`,
{ title: post.title,
@@ -63,7 +73,7 @@ export const updatePost = async (
parent_post_ids: post.parentPostIds,
original_created_from: post.originalCreatedFrom,
original_created_before: post.originalCreatedBefore },
{ params: { ...(baseVersionNo && { base_version_no: String (baseVersionNo) }),
{ locale, params: { ...(baseVersionNo && { base_version_no: String (baseVersionNo) }),
force: force ? '1' : '0',
merge: merge ? '1' : '0' } })
+64 -51
ファイルの表示
@@ -5,7 +5,9 @@ import { getHelpArticle,
getHelpChanges,
getHelpDiff,
getHelpEditionContent,
getHelpSearchEntries } from '@/lib/help'
getHelpSearchEntries,
resolveHelpLanguage } from '@/lib/help'
import { localeFromPath, localeLanguage, unlocalisedPath } from '@/lib/locales'
import { fetchPost, fetchPosts, fetchPostChanges } from '@/lib/posts'
import { helpKeys, postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
import { fetchNicoTags, fetchTagByName, fetchTag, fetchTagChanges, fetchTags } from '@/lib/tags'
@@ -13,9 +15,10 @@ import { fetchWikiPage,
fetchWikiPageByTitle,
fetchWikiPages } from '@/lib/wiki'
import type { LocaleCode } from '@/lib/locales'
import type { Category, FetchPostsOrder, FetchTagsOrder } from '@/types'
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
type Prefetcher = (qc: QueryClient, url: URL, locale: LocaleCode) => Promise<void>
const mPost = match<{ id: string }> ('/posts/:id')
const mWiki = match<{ title: string }> ('/wiki/:title')
@@ -28,25 +31,25 @@ const boolFromQuery = (value: string | null): boolean =>
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
const prefetchHelpIndex: Prefetcher = async qc => {
const prefetchHelpIndex: Prefetcher = async (qc, url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.index ('ja'),
queryFn: () => getHelpSearchEntries ('ja'),
queryKey: helpKeys.index (localeLanguage (locale)),
queryFn: () => getHelpSearchEntries (localeLanguage (locale)),
staleTime: Infinity })
}
const prefetchHelpShow: Prefetcher = async (qc, url) => {
const prefetchHelpShow: Prefetcher = async (qc, url, locale) => {
const matched = mHelp (url.pathname)
if (!(matched))
return
const { identifier } = matched.params
const language = 'ja'
const article = await qc.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
const edition = url.searchParams.get ('edition') ?? article?.languages[language]?.currentEdition
if (edition == null)
return
@@ -58,15 +61,15 @@ const prefetchHelpShow: Prefetcher = async (qc, url) => {
}
const prefetchHelpChanges: Prefetcher = async qc => {
const prefetchHelpChanges: Prefetcher = async (qc, url, locale) => {
await qc.prefetchQuery ({
queryKey: helpKeys.changes ('ja'),
queryFn: () => getHelpChanges ('ja'),
queryKey: helpKeys.changes (localeLanguage (locale)),
queryFn: () => getHelpChanges (localeLanguage (locale)),
staleTime: Infinity })
}
const prefetchHelpDiff: Prefetcher = async (qc, url) => {
const prefetchHelpDiff: Prefetcher = async (qc, url, locale) => {
const matched = mHelpDiff (url.pathname)
const fromValues = url.searchParams.getAll ('from')
const toValues = url.searchParams.getAll ('to')
@@ -76,23 +79,28 @@ const prefetchHelpDiff: Prefetcher = async (qc, url) => {
return
const { identifier } = matched.params
const article = await qc.fetchQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
await qc.prefetchQuery ({
queryKey: helpKeys.diff (identifier, 'ja', from, to),
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
queryKey: helpKeys.diff (identifier, language, from, to),
queryFn: () => getHelpDiff (identifier, language, from, to),
staleTime: Infinity })
}
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
const prefetchWikiPagesIndex: Prefetcher = async (qc, url, locale) => {
const title = url.searchParams.get ('title') ?? ''
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ title }),
queryFn: () => fetchWikiPages ({ title }) })
queryKey: wikiKeys.index ({ title }, locale),
queryFn: () => fetchWikiPages ({ title }, locale) })
}
const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
const prefetchWikiPageShow: Prefetcher = async (qc, url, locale) => {
const m = mWiki (url.pathname)
if (!(m))
return
@@ -101,29 +109,29 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
const version = url.searchParams.get ('version') || undefined
const wikiPage = await qc.fetchQuery ({
queryKey: wikiKeys.show (title, { version }),
queryFn: () => fetchWikiPageByTitle (title, { version }) })
queryKey: wikiKeys.show (title, { version }, locale),
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
if (wikiPage)
{
const effectiveId = String (wikiPage.id ?? '')
await qc.prefetchQuery ({
queryKey: wikiKeys.show (effectiveId, { }),
queryFn: () => fetchWikiPage (effectiveId, { } ) })
queryKey: wikiKeys.show (effectiveId, { }, locale),
queryFn: () => fetchWikiPage (effectiveId, { }, locale ) })
if (wikiPage.body)
{
await qc.prefetchQuery ({
queryKey: wikiKeys.index ({ }),
queryFn: () => fetchWikiPages ({ }) })
queryKey: wikiKeys.index ({ }, locale),
queryFn: () => fetchWikiPages ({ }, locale) })
}
}
const effectiveTitle = wikiPage?.title ?? title
await qc.prefetchQuery ({
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => fetchTagByName (effectiveTitle) })
queryKey: tagsKeys.show (effectiveTitle, locale),
queryFn: () => fetchTagByName (effectiveTitle, locale) })
if (version)
return
@@ -143,12 +151,12 @@ const prefetchWikiPageShow: Prefetcher = async (qc, url) => {
updatedTo: '',
order: 'original_created_at:desc' } as const
await qc.prefetchQuery ({
queryKey: postsKeys.index (p),
queryFn: () => fetchPosts (p) })
queryKey: postsKeys.index (p, locale),
queryFn: () => fetchPosts (p, locale) })
}
const prefetchPostsIndex: Prefetcher = async (qc, url) => {
const prefetchPostsIndex: Prefetcher = async (qc, url, locale) => {
const tags = url.searchParams.get ('tags') ?? ''
const qURL = url.searchParams.get ('url') ?? ''
const title = url.searchParams.get ('title') ?? ''
@@ -169,12 +177,12 @@ const prefetchPostsIndex: Prefetcher = async (qc, url) => {
updatedFrom, updatedTo, order }
await qc.prefetchQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
}
const prefetchPostShow: Prefetcher = async (qc, url) => {
const prefetchPostShow: Prefetcher = async (qc, url, locale) => {
const m = mPost (url.pathname)
if (!(m))
return
@@ -182,12 +190,12 @@ const prefetchPostShow: Prefetcher = async (qc, url) => {
const { id } = m.params
await qc.prefetchQuery ({
queryKey: postsKeys.show (id),
queryFn: () => fetchPost (id) })
queryKey: postsKeys.show (id, locale),
queryFn: () => fetchPost (id, locale) })
}
const prefetchPostChanges: Prefetcher = async (qc, url) => {
const prefetchPostChanges: Prefetcher = async (qc, url, locale) => {
const id = url.searchParams.get ('id')
const tag = url.searchParams.get ('tag')
const externalTag = url.searchParams.get ('external_tag')
@@ -197,23 +205,23 @@ const prefetchPostChanges: Prefetcher = async (qc, url) => {
if (tag)
{
await qc.prefetchQuery ({
queryKey: tagsKeys.show (tag),
queryFn: () => fetchTag (tag) })
queryKey: tagsKeys.show (tag, locale),
queryFn: () => fetchTag (tag, locale) })
}
await qc.prefetchQuery ({
queryKey: postsKeys.changes ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }),
page, limit }, locale),
queryFn: () => fetchPostChanges ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }) })
page, limit }, locale) })
}
const prefetchTagsIndex: Prefetcher = async (qc, url) => {
const prefetchTagsIndex: Prefetcher = async (qc, url, locale) => {
const postRaw = url.searchParams.get ('post')
const post = postRaw ? Number (postRaw) : null
const name = url.searchParams.get ('name') ?? ''
@@ -237,12 +245,12 @@ const prefetchTagsIndex: Prefetcher = async (qc, url) => {
updatedFrom, updatedTo, deprecated, page, limit, order }
await qc.prefetchQuery ({
queryKey: tagsKeys.index (keys),
queryFn: () => fetchTags (keys) })
queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) })
}
const prefetchNicoTagsIndex: Prefetcher = async (qc, url) => {
const prefetchNicoTagsIndex: Prefetcher = async (qc, url, locale) => {
const keys = {
name: url.searchParams.get ('name') ?? '',
linkedTag: url.searchParams.get ('linked_tag') ?? '',
@@ -255,12 +263,12 @@ const prefetchNicoTagsIndex: Prefetcher = async (qc, url) => {
| 'updated_at:asc' | 'updated_at:desc' }
await qc.prefetchQuery ({
queryKey: tagsKeys.nicoIndex (keys),
queryFn: () => fetchNicoTags (keys) })
queryKey: tagsKeys.nicoIndex (keys, locale),
queryFn: () => fetchNicoTags (keys, locale) })
}
const prefetchTagShow: Prefetcher = async (qc, url) => {
const prefetchTagShow: Prefetcher = async (qc, url, locale) => {
const m = mTag (url.pathname)
if (!(m))
return
@@ -268,19 +276,19 @@ const prefetchTagShow: Prefetcher = async (qc, url) => {
const { id } = m.params
await qc.prefetchQuery ({
queryKey: tagsKeys.show (id),
queryFn: () => fetchTag (id) })
queryKey: tagsKeys.show (id, locale),
queryFn: () => fetchTag (id, locale) })
}
const prefetchTagChanges: Prefetcher = async (qc, url) => {
const prefetchTagChanges: Prefetcher = async (qc, url, locale) => {
const id = url.searchParams.get ('id')
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
await qc.prefetchQuery ({
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }) })
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }, locale),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }, locale) })
}
@@ -313,9 +321,14 @@ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[]
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
const u = new URL (urlLike, location.origin)
if (u.origin !== location.origin)
return
const locale = localeFromPath (u.pathname) ?? 'ja'
u.pathname = unlocalisedPath (u.pathname)
const r = routePrefetchers.find (x => x.test (u))
if (!(r))
return
await r.run (qc, u)
await r.run (qc, u, locale)
}
+97 -30
ファイルの表示
@@ -1,3 +1,4 @@
import type { LocaleCode } from '@/lib/locales'
import type {
FetchNicoTagsParams,
FetchMaterialsParams,
@@ -8,59 +9,125 @@ import type {
export const postsKeys = {
root: ['posts'] as const,
index: (p: FetchPostsParams) => ['posts', 'index', p] as const,
show: (id: string) => ['posts', id] as const,
related: (id: string) => ['related', id] as const,
changes: (p: { post?: string
index: (
p: FetchPostsParams,
locale: LocaleCode = 'ja',
) => ['posts', locale, 'index', p] as const,
show: (
id: string,
locale: LocaleCode = 'ja',
) => ['posts', locale, id] as const,
related: (
id: string,
locale: LocaleCode = 'ja',
) => ['related', locale, id] as const,
changes: (
p: { post?: string
tag?: string
externalTag?: string
page: number
limit: number }) =>
['posts', 'changes', p] as const }
limit: number },
locale: LocaleCode = 'ja',
) =>
['posts', locale, 'changes', p] as const }
export const gekanatorKeys = {
root: ['gekanator'] as const,
posts: () => ['gekanator', 'posts'] as const,
questions: () => ['gekanator', 'questions'] as const,
extraQuestions: (gameId: number, nonce: string) =>
['gekanator', 'games', gameId, 'extra-questions', nonce] as const }
posts: (
locale: LocaleCode = 'ja',
) => ['gekanator', locale, 'posts'] as const,
questions: (
locale: LocaleCode = 'ja',
) => ['gekanator', locale, 'questions'] as const,
extraQuestions: (
gameId: number,
nonce: string,
locale: LocaleCode = 'ja',
) =>
['gekanator', locale, 'games', gameId, 'extra-questions', nonce] as const }
export const tagsKeys = {
root: ['tags'] as const,
index: (p: FetchTagsParams) => ['tags', 'index', p] as const,
index: (
p: FetchTagsParams,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'index', p] as const,
nicoRoot: ['tags', 'nico'] as const,
nicoIndex: (p: FetchNicoTagsParams) => ['tags', 'nico', 'index', p] as const,
externalShow: (id: string) => ['tags', 'nico', id] as const,
show: (name: string) => ['tags', name] as const,
changes: (p: { id?: string; page: number; limit: number }) =>
['tags', 'changes', p] as const,
deerjikists: (id: string) => ['tags', 'deerjikists', id] as const }
nicoIndex: (
p: FetchNicoTagsParams,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'nico', 'index', p] as const,
externalShow: (
id: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'nico', id] as const,
show: (
name: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, name] as const,
changes: (
p: { id?: string; page: number; limit: number },
locale: LocaleCode = 'ja',
) =>
['tags', locale, 'changes', p] as const,
deerjikists: (
id: string,
locale: LocaleCode = 'ja',
) => ['tags', locale, 'deerjikists', id] as const }
export const materialsKeys = {
root: ['materials'] as const,
index: (p: FetchMaterialsParams) => ['materials', 'index', p] as const,
changes: (p: {
index: (
p: FetchMaterialsParams,
locale: LocaleCode = 'ja',
) => ['materials', locale, 'index', p] as const,
changes: (
p: {
materialId?: string
tag?: string
eventType?: string
page: number
limit: number
}) => ['materials', 'changes', p] as const,
byTagName: (name: string, materialFilter: MaterialFilter) =>
['materials', 'tag', name, materialFilter] as const,
show: (id: string) => ['materials', id] as const,
suppressions: () => ['materials', 'suppressions'] as const,
tree: (p: {
},
locale: LocaleCode = 'ja',
) => ['materials', locale, 'changes', p] as const,
byTagName: (
name: string,
materialFilter: MaterialFilter,
locale: LocaleCode = 'ja',
) =>
['materials', locale, 'tag', name, materialFilter] as const,
show: (
id: string,
locale: LocaleCode = 'ja',
) => ['materials', locale, id] as const,
suppressions: (
locale: LocaleCode = 'ja',
) => ['materials', locale, 'suppressions'] as const,
tree: (
p: {
parentId?: number | null
materialFilter: MaterialFilter
}) => ['materials', 'tree', p] as const,
unclassified: (p: { page?: number; limit?: number } = { }) =>
['materials', 'unclassified', p] as const}
},
locale: LocaleCode = 'ja',
) => ['materials', locale, 'tree', p] as const,
unclassified: (
p: { page?: number; limit?: number } = { },
locale: LocaleCode = 'ja',
) =>
['materials', locale, 'unclassified', p] as const}
export const wikiKeys = {
root: ['wiki'] as const,
index: (p: { title?: string }) => ['wiki', 'index', p] as const,
show: (title: string, p: { version?: string }) => ['wiki', title, p] as const }
index: (
p: { title?: string },
locale: LocaleCode = 'ja',
) => ['wiki', locale, 'index', p] as const,
show: (
title: string,
p: { version?: string },
locale: LocaleCode = 'ja',
) => ['wiki', locale, title, p] as const }
export const helpKeys = {
root: ['help'] as const,
+32 -14
ファイルの表示
@@ -2,6 +2,7 @@ import { isAxiosError } from 'axios'
import { apiGet } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { Deerjikist,
FetchNicoTagsParams,
FetchTagsParams,
@@ -13,9 +14,11 @@ import type { Deerjikist,
export const fetchTags = async (
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
updatedFrom, updatedTo, deprecated,
page, limit, order }: FetchTagsParams): Promise<{ tags: Tag[]
page, limit, order }: FetchTagsParams,
locale: LocaleCode = 'ja',
): Promise<{ tags: Tag[]
count: number }> =>
await apiGet ('/tags', { params: {
await apiGet ('/tags', { locale, params: {
...(post != null && { post }),
...(name && { name }),
...(category && { category }),
@@ -32,9 +35,11 @@ export const fetchTags = async (
export const fetchNicoTags = async (
{ name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams): Promise<{ tags: NicoTag[]
{ name, linkedTag, linkStatus, page, limit, order }: FetchNicoTagsParams,
locale: LocaleCode = 'ja',
): Promise<{ tags: NicoTag[]
count: number }> =>
await apiGet ('/tags/nico', { params: {
await apiGet ('/tags/nico', { locale, params: {
page,
limit,
name,
@@ -43,10 +48,13 @@ export const fetchNicoTags = async (
order } })
export const fetchTag = async (id: string): Promise<Tag | null> => {
export const fetchTag = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/${ id }`)
return await apiGet (`/tags/${ id }`, { locale })
}
catch
{
@@ -55,10 +63,13 @@ export const fetchTag = async (id: string): Promise<Tag | null> => {
}
export const fetchExternalTag = async (id: string): Promise<Tag | null> => {
export const fetchExternalTag = async (
id: string,
locale: LocaleCode = 'ja',
): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/nico/${ id }`)
return await apiGet (`/tags/nico/${ id }`, { locale })
}
catch
{
@@ -67,10 +78,13 @@ export const fetchExternalTag = async (id: string): Promise<Tag | null> => {
}
export const fetchTagByName = async (name: string): Promise<Tag | null> => {
export const fetchTagByName = async (
name: string,
locale: LocaleCode = 'ja',
): Promise<Tag | null> => {
try
{
return await apiGet (`/tags/name/${ encodeURIComponent (name) }`)
return await apiGet (`/tags/name/${ encodeURIComponent (name) }`, { locale })
}
catch (error)
{
@@ -84,12 +98,16 @@ export const fetchTagChanges = async (
{ id, page, limit }: {
id?: string
page: number
limit: number }): Promise<{
limit: number },
locale: LocaleCode = 'ja',
): Promise<{
versions: TagVersion[]
count: number }> =>
await apiGet ('/tags/versions', { params: { ...(id && { id }), page, limit } })
await apiGet ('/tags/versions', { locale, params: { ...(id && { id }), page, limit } })
export const fetchDeerjikistsByTag = async (
id: string): Promise<{ tag: Tag; deerjikists: Deerjikist[] }> =>
await apiGet (`/tags/${ id }/deerjikists`)
id: string,
locale: LocaleCode = 'ja',
): Promise<{ tag: Tag; deerjikists: Deerjikist[] }> =>
await apiGet (`/tags/${ id }/deerjikists`, { locale })
+3 -2
ファイルの表示
@@ -7,8 +7,9 @@ import {
useRef,
useState,
} from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocaleNavigate } from '@/lib/useLocale'
import ShortcutHelpDialog from '@/components/users/ShortcutHelpDialog'
import {
DEFAULT_KEY_BINDINGS,
@@ -95,7 +96,7 @@ const focusSearchTarget = (): void => {
export const KeyboardShortcutsProvider = ({ children }: PropsWithChildren) => {
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [keyboardSettings, setKeyboardSettingsState] =
useState<ClientKeyboardSettings> (() => getClientKeyboardSettings ())
+27
ファイルの表示
@@ -0,0 +1,27 @@
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { isLocaleCode, localeFromPath, localiseTo } from '@/lib/locales'
import type { NavigateFunction, NavigateOptions, To } from 'react-router-dom'
import type { LocaleCode } from '@/lib/locales'
export const useLocale = (): LocaleCode => {
const { locale } = useParams ()
const location = useLocation ()
return isLocaleCode (locale) ? locale : localeFromPath (location.pathname) ?? 'ja'
}
export const useLocaleNavigate = (): NavigateFunction => {
const navigate = useNavigate ()
const locale = useLocale ()
return useCallback ((to: To | number, options?: NavigateOptions) => {
if (typeof to === 'number')
navigate (to)
else
navigate (localiseTo (to, locale), options)
}, [locale, navigate])
}
+13 -6
ファイルの表示
@@ -2,26 +2,33 @@ import { isAxiosError } from 'axios'
import { apiGet } from '@/lib/api'
import type { LocaleCode } from '@/lib/locales'
import type { WikiPage } from '@/types'
export const fetchWikiPages = async (
{ title }: { title?: string }): Promise<WikiPage[]> =>
await apiGet ('/wiki', { params: { title } })
{ title }: { title?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage[]> =>
await apiGet ('/wiki', { locale, params: { title } })
export const fetchWikiPage = async (
id: string,
{ version }: { version?: string }): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`, { params: version ? { version } : { } })
{ version }: { version?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage> =>
await apiGet (`/wiki/${ id }`, { locale, params: version ? { version } : { } })
export const fetchWikiPageByTitle = async (
title: string,
{ version }: { version?: string }): Promise<WikiPage | null> => {
{ version }: { version?: string },
locale: LocaleCode = 'ja',
): Promise<WikiPage | null> => {
try
{
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { params: { version } })
return await apiGet (`/wiki/title/${ encodeURIComponent (title) }`, { locale, params: { version } })
}
catch (error)
{
+13 -11
ファイルの表示
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
@@ -3678,6 +3679,7 @@ const expectedAnswerFor = (
const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const storedGame = useMemo (loadStoredGame, [])
@@ -3771,8 +3773,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const [history, setHistory] = useState<GameSnapshot[]> ([])
const { data: posts = [], isLoading, error } = useQuery ({
queryKey: gekanatorKeys.posts (),
queryFn: fetchGekanatorPosts,
queryKey: gekanatorKeys.posts (locale),
queryFn: () => fetchGekanatorPosts (locale),
refetchOnWindowFocus: false })
const {
data: acceptedQuestions = [],
@@ -3780,8 +3782,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
isLoading: acceptedQuestionsLoading,
error: acceptedQuestionsError
} = useQuery ({
queryKey: gekanatorKeys.questions (),
queryFn: fetchGekanatorQuestions,
queryKey: gekanatorKeys.questions (locale),
queryFn: () => fetchGekanatorQuestions (locale),
select: questions => questions.map (restoreGekanatorQuestion),
refetchOnWindowFocus: false })
const materialIndex = useMemo (() => buildMaterialIndex (posts), [posts])
@@ -4103,7 +4105,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const mascotAsset = mascotAssetByState[mascot]
const mascotAlt = mascotAltByState[mascot]
const saveMutation = useMutation ({
mutationFn: saveGekanatorGame,
mutationFn: (variables: Parameters<typeof saveGekanatorGame>[0]) => saveGekanatorGame (variables, locale),
onSuccess: (data, variables) => {
setRecentGames (storeRecentGameSummary ({
correctPostId: variables.correctPostId,
@@ -4114,9 +4116,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setLearnedExampleCount (data.learnedExampleCount)
setResultWon (variables.guessedPostId === variables.correctPostId)}})
const questionSuggestionMutation = useMutation ({
mutationFn: saveGekanatorQuestionSuggestion,
mutationFn: (variables: Parameters<typeof saveGekanatorQuestionSuggestion>[0]) => saveGekanatorQuestionSuggestion (variables, locale),
onSuccess: async data => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions (locale) })
setQuestionSuggestionCount (data.count)
setQuestionSuggestionEntryMode ('search')
setQuestionSuggestionSearch ('')
@@ -4124,9 +4126,9 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
setQuestionSuggestion ('')
setQuestionSuggestionAnswer ('yes')}})
const extraQuestionAnswersMutation = useMutation ({
mutationFn: saveGekanatorExtraQuestionAnswers,
mutationFn: (variables: Parameters<typeof saveGekanatorExtraQuestionAnswers>[0]) => saveGekanatorExtraQuestionAnswers (variables, locale),
onSuccess: async () => {
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions () })
await queryClient.refetchQueries ({ queryKey: gekanatorKeys.questions (locale) })
setExtraQuestionState ('saved')
setPhase ('end')}})
@@ -4784,8 +4786,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
try
{
const questions = await queryClient.fetchQuery ({
queryKey: gekanatorKeys.extraQuestions (gameId, nonce),
queryFn: () => fetchGekanatorExtraQuestions (gameId, nonce) })
queryKey: gekanatorKeys.extraQuestions (gameId, nonce, locale),
queryFn: () => fetchGekanatorExtraQuestions (gameId, nonce, locale) })
setExtraQuestions (questions.slice (0, 2))
setExtraQuestionState (questions.length > 0 ? 'ready' : 'empty')
}
+5 -3
ファイルの表示
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
@@ -24,12 +25,13 @@ type DeerjikistFormField =
const DeerjikistDetailPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const tagId = String (id ?? '')
const tagKey = tagsKeys.deerjikists (tagId)
const tagKey = tagsKeys.deerjikists (tagId, locale)
const { data: qData, isLoading: loading } =
useQuery ({ queryKey: tagKey, queryFn: () => fetchDeerjikistsByTag (tagId) })
useQuery ({ queryKey: tagKey, queryFn: () => fetchDeerjikistsByTag (tagId, locale) })
const tag = qData?.tag
const deerjikists = useMemo (() => qData?.deerjikists ?? [], [qData])
@@ -49,7 +51,7 @@ const DeerjikistDetailPage: FC = () => {
{
setDisabled (true)
setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data))
setData (await apiPut<Deerjikist[]> (`/tags/${ id }/deerjikists`, data, { locale }))
qc.invalidateQueries ({ queryKey: tagsKeys.root })
toast ({ description: '更新しました.' })
+5 -2
ファイルの表示
@@ -9,6 +9,8 @@ import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { getHelpChanges, helpEditionDate } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { helpKeys } from '@/lib/queryKeys'
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
import { dateString } from '@/lib/utils'
@@ -17,6 +19,7 @@ import type { FC } from 'react'
const HelpChangesPage: FC = () => {
const language = localeLanguage (useLocale ())
const [searchParams, setSearchParams] = useSearchParams ()
const identifier = searchParams.get ('identifier') ?? ''
const pageValue = searchParams.get ('page') ?? '1'
@@ -27,8 +30,8 @@ const HelpChangesPage: FC = () => {
const limit = LIST_LIMIT_OPTIONS.find (
value => String (value) === searchParams.get ('limit')) ?? 20
const { data: changes = [], isLoading, error } = useQuery ({
queryKey: helpKeys.changes ('ja'),
queryFn: () => getHelpChanges ('ja'),
queryKey: helpKeys.changes (language),
queryFn: () => getHelpChanges (language),
staleTime: Infinity })
const results = useMemo (() => identifier
+8 -5
ファイルの表示
@@ -8,7 +8,10 @@ import { SITE_TITLE } from '@/config'
import { getHelpArticle,
getHelpEditionContent,
helpEditionDate,
parseHelpEdition } from '@/lib/help'
parseHelpEdition,
resolveHelpLanguage } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { dateString } from '@/lib/utils'
import { helpKeys } from '@/lib/queryKeys'
import NotFound from '@/pages/NotFound'
@@ -17,17 +20,17 @@ import type { FC } from 'react'
const HelpDetailPage: FC = () => {
const locale = useLocale ()
const { identifier = '' } = useParams ()
const location = useLocation ()
// Locale 対応が入るまでは ja 固定
const language = 'ja'
const { data: article, isLoading: articleLoading } = useQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
const query = new URLSearchParams (location.search)
const requestedEdition = query.get ('edition')
@@ -96,7 +99,7 @@ const HelpDetailPage: FC = () => {
: '(最新)'}
</div>)}
<article className="prose dark:prose-invert mx-auto p-4">
<article lang={language} className="prose dark:prose-invert mx-auto p-4">
{Content ? <Content/> : <div>Loading...</div>}
</article>
</MainArea>)
+13 -5
ファイルの表示
@@ -6,7 +6,9 @@ import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { getHelpDiff } from '@/lib/help'
import { getHelpArticle, getHelpDiff, resolveHelpLanguage } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { helpKeys } from '@/lib/queryKeys'
import { cn } from '@/lib/utils'
import NotFound from '@/pages/NotFound'
@@ -17,17 +19,23 @@ const markers = { added: '+ ', removed: '- ', unchanged: ' ' }
const HelpDiffPage: FC = () => {
const locale = useLocale ()
const { identifier = '' } = useParams ()
const [searchParams] = useSearchParams ()
const from = searchParams.get ('from') ?? ''
const to = searchParams.get ('to') ?? ''
const { data: article } = useQuery ({
queryKey: helpKeys.article (identifier),
queryFn: () => getHelpArticle (identifier),
staleTime: Infinity })
const language = resolveHelpLanguage (article, localeLanguage (locale))
const validParams = Boolean (identifier && from && to)
&& searchParams.getAll ('from').length === 1
&& searchParams.getAll ('to').length === 1
const { data: diff, isLoading, isError } = useQuery ({
enabled: validParams,
queryKey: helpKeys.diff (identifier, 'ja', from, to),
queryFn: () => getHelpDiff (identifier, 'ja', from, to),
enabled: validParams && article != null,
queryKey: helpKeys.diff (identifier, language, from, to),
queryFn: () => getHelpDiff (identifier, language, from, to),
staleTime: Infinity })
if (!(validParams) || isError || (!(isLoading) && diff == null))
@@ -55,7 +63,7 @@ const HelpDiffPage: FC = () => {
{diff.to.title} ({to})
</PrefetchLink>
</div>
<pre className="mt-4 overflow-x-auto whitespace-pre">
<pre lang={language} className="mt-4 overflow-x-auto whitespace-pre">
<code>
{diff.lines.map ((line, index) => {
const colour = cn (
+5 -2
ファイルの表示
@@ -15,6 +15,8 @@ import { compareHelpEditions,
getHelpSearchEntries,
helpEditionDate,
normaliseHelpSearchText } from '@/lib/help'
import { localeLanguage } from '@/lib/locales'
import { useLocale } from '@/lib/useLocale'
import { helpKeys } from '@/lib/queryKeys'
import { LIST_LIMIT_OPTIONS } from '@/lib/settings'
import { dateString, inputClass } from '@/lib/utils'
@@ -30,6 +32,7 @@ const defaultDirection: Record<HelpOrderField, 'asc' | 'desc'> = {
const HelpSearchPage: FC = () => {
const language = localeLanguage (useLocale ())
const [searchParams, setSearchParams] = useSearchParams ()
const pageValue = searchParams.get ('page') ?? '1'
const pageNumber = Number (pageValue)
@@ -49,8 +52,8 @@ const HelpSearchPage: FC = () => {
const [title, setTitle] = useState (conditions.title)
const [text, setText] = useState (conditions.text)
const { data: entries = [], isLoading: loading, error } = useQuery ({
queryKey: helpKeys.index ('ja'),
queryFn: () => getHelpSearchEntries ('ja'),
queryKey: helpKeys.index (language),
queryFn: () => getHelpSearchEntries (language),
staleTime: Infinity })
useEffect (() => {
+6 -4
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import WikiBody from '@/components/WikiBody'
import FieldError from '@/components/common/FieldError'
@@ -28,6 +29,7 @@ type MaterialFormField = 'tag' | 'file' | 'url' | 'exportPaths'
const MaterialDetailPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const qc = useQueryClient ()
@@ -40,8 +42,8 @@ const MaterialDetailPage: FC = () => {
useValidationErrors<MaterialFormField> ()
const { data: material, isError, isLoading } = useQuery ({
queryKey: materialsKeys.show (id ?? ''),
queryFn: () => fetchMaterial (id ?? ''),
queryKey: materialsKeys.show (id ?? '', locale),
queryFn: () => fetchMaterial (id ?? '', locale),
enabled: id != null})
const materialTitle = material
? material.tag?.name ?? `素材 #${ material.id }`
@@ -76,10 +78,10 @@ const MaterialDetailPage: FC = () => {
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
return await updateMaterial (id ?? '', formData)
return await updateMaterial (id ?? '', formData, locale)
},
onSuccess: async data => {
qc.setQueryData (materialsKeys.show (id ?? ''), data)
qc.setQueryData (materialsKeys.show (id ?? '', locale), data)
await invalidateMaterialQueries ()
toast ({ title: '更新成功!' })
},
+6 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
@@ -19,10 +20,11 @@ import type { FC, FormEvent } from 'react'
const MaterialHistoryPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = new URLSearchParams (location.search)
const materialId = query.get ('material_id') ?? ''
@@ -47,13 +49,13 @@ const MaterialHistoryPage: FC = () => {
...(tag && { tag }),
...(eventType && { eventType }),
page,
limit }),
limit }, locale),
queryFn: () => fetchMaterialChanges ({
...(materialId && { materialId }),
...(tag && { tag }),
...(eventType && { eventType }),
page,
limit }) })
limit }, locale) })
const versions = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+6 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import nikumaru from '@/assets/fonts/nikumaru.otf'
import PrefetchLink from '@/components/PrefetchLink'
import DateTimeField from '@/components/common/DateTimeField'
@@ -210,8 +211,9 @@ const GroupHeading: FC<{
const MaterialListPage: FC = () => {
const locale = useLocale ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
const materialFilter = parseMaterialFilter (query.get ('material_filter'), 'present')
@@ -277,8 +279,8 @@ const MaterialListPage: FC = () => {
page,
limit}
const { data, isLoading, isError } = useQuery ({
queryKey: materialsKeys.index (keys),
queryFn: () => fetchMaterials (keys)})
queryKey: materialsKeys.index (keys, locale),
queryFn: () => fetchMaterials (keys, locale)})
const tagScope = data?.tagScope ?? null
const materials = data?.materials ?? []
const groups = data?.groups ?? []
+8 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
@@ -13,6 +14,7 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { createMaterial } from '@/lib/materials'
import { unlocalisedPath } from '@/lib/locales'
import { materialsKeys } from '@/lib/queryKeys'
import { inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
@@ -23,14 +25,16 @@ type MaterialFormField = 'tag' | 'file' | 'url' | 'exportPaths'
const MaterialNewPage: FC = () => {
const locale = useLocale ()
const qc = useQueryClient ()
const location = useLocation ()
const query = new URLSearchParams (location.search)
const tagQuery = query.get ('tag') ?? ''
const returnToQuery = query.get ('return_to') ?? ''
const safeReturnTo = returnToQuery.startsWith ('/materials') ? returnToQuery : ''
const safeReturnTo =
unlocalisedPath (returnToQuery).startsWith ('/materials') ? returnToQuery : ''
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [file, setFile] = useState<File | null> (null)
const [filePreview, setFilePreview] = useState ('')
@@ -51,7 +55,7 @@ const MaterialNewPage: FC = () => {
formData.append ('url', url)
formData.append ('export_paths[legacy_drive]', exportPath)
return await createMaterial (formData)
return await createMaterial (formData, locale)
},
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
+6 -4
ファイルの表示
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
@@ -52,6 +53,7 @@ const reasonName = (reason: string): string =>
const MaterialSyncSuppressionsPage: FC = () => {
const locale = useLocale ()
const qc = useQueryClient ()
const [sourceKind, setSourceKind] =
useState<MaterialSyncSuppressionSourceKind> ('uri')
@@ -61,8 +63,8 @@ const MaterialSyncSuppressionsPage: FC = () => {
const [reason, setReason] = useState<MaterialSyncSuppressionReason> (REASONS[0])
const { data, isError, isLoading } = useQuery ({
queryKey: materialsKeys.suppressions (),
queryFn: fetchMaterialSyncSuppressions})
queryKey: materialsKeys.suppressions (locale),
queryFn: () => fetchMaterialSyncSuppressions (locale)})
const createMutation = useMutation ({
mutationFn: async () =>
@@ -71,9 +73,9 @@ const MaterialSyncSuppressionsPage: FC = () => {
sourceUri,
drivePath,
driveFileId,
reason}),
reason}, locale),
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions () })
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions (locale) })
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
setSourceUri ('')
setDrivePath ('')
+5 -3
ファイルの表示
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PostEditForm from '@/components/PostEditForm'
import PostEmbed from '@/components/PostEmbed'
import PostList from '@/components/PostList'
@@ -29,6 +30,7 @@ type Props = { user: User | null }
const PostDetailPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const behaviourSettings = useClientBehaviourSettings ()
const thumbnailMode = behaviourSettings.thumbnailMode ?? 'normal'
@@ -42,12 +44,12 @@ const PostDetailPage: FC<Props> = ({ user }) => {
? { duration: .08, ease: 'linear' as const }
: { duration: .2, ease: 'easeOut' as const }
const postId = String (id ?? '')
const postKey = postsKeys.show (postId)
const postKey = postsKeys.show (postId, locale)
const { data: post, isError: errorFlg, error } = useQuery ({
enabled: Boolean (id),
queryKey: postKey,
queryFn: () => fetchPost (postId) })
queryFn: () => fetchPost (postId, locale) })
const qc = useQueryClient ()
@@ -181,7 +183,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
<PostEditForm
post={post}
onSave={newPost => {
qc.setQueryData (postsKeys.show (postId),
qc.setQueryData (postsKeys.show (postId, locale),
(prev: Post | undefined) => newPost ?? prev)
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
+9 -7
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
@@ -39,6 +40,7 @@ const renderDiff = (diff: { current: string | null; prev: string | null }) => (
const PostHistoryPage: FC = () => {
const locale = useLocale ()
const dialogue = useDialogue ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
@@ -61,25 +63,25 @@ const PostHistoryPage: FC = () => {
const tagQueryId = tagId ?? ''
const { data: tag } = useQuery ({
enabled: Boolean (tagId),
queryKey: tagsKeys.show (tagQueryId),
queryFn: () => fetchTag (tagQueryId) })
queryKey: tagsKeys.show (tagQueryId, locale),
queryFn: () => fetchTag (tagQueryId, locale) })
const externalTagQueryId = externalTagId ?? ''
const { data: externalTag } = useQuery ({
enabled: Boolean (externalTagId),
queryKey: tagsKeys.externalShow (externalTagQueryId),
queryFn: () => fetchExternalTag (externalTagQueryId) })
queryKey: tagsKeys.externalShow (externalTagQueryId, locale),
queryFn: () => fetchExternalTag (externalTagQueryId, locale) })
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.changes ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && { externalTag: externalTagId }),
page, limit }),
page, limit }, locale),
queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && {
externalTag: externalTagId }),
page, limit }) })
page, limit }, locale) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
const displayedTag = externalTag ?? tag
@@ -121,7 +123,7 @@ const PostHistoryPage: FC = () => {
const originalCreatedBefore = change.originalCreatedBefore.current
await updatePost ({ id, title, tags, duration, parentPostIds,
originalCreatedFrom, originalCreatedBefore },
{ force: true })
{ force: true }, locale)
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
+10 -8
ファイルの表示
@@ -2,8 +2,9 @@ import { AnimatePresence, motion } from 'framer-motion'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { ChevronRight } from 'lucide-react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PrefetchLink from '@/components/PrefetchLink'
@@ -595,10 +596,11 @@ const editableRowDirty = (row: PostImportRow): boolean =>
const PostImportReviewPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const dialogue = useDialogue ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const existingRowsTransition =
@@ -696,7 +698,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
try
{
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', {
const preview = await apiGet<PostMetadataResponse> ('/posts/metadata', { locale,
params: { url: currentRow.url },
signal: controller.signal })
if (!(active) || metadataSequenceRef.current !== sequence)
@@ -770,7 +772,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
active = false
controllers.forEach (controller => controller.abort ())
}
}, [commitRows, currentRowBySource, location.search, updateRows])
}, [commitRows, currentRowBySource, location.search, updateRows, locale])
useEffect (() => {
if (editingRow == null || resultRepairMode (rowsRef.current) !== 'failed')
@@ -871,12 +873,12 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
urlChanged
? mergePreviewRow (
nextRow,
await apiGet<PostMetadataResponse> ('/posts/metadata', {
await apiGet<PostMetadataResponse> ('/posts/metadata', { locale,
params: { url: nextRow.url } }))
: nextRow
const dryRun = await apiPost<PostMetadataResponse> (
'/posts?dry=1',
buildDryRunFormData (candidateRow))
buildDryRunFormData (candidateRow), { locale })
const mergedRow = mergeDryRunRow (candidateRow, dryRun)
const mergedRows = updateRows (currentRows =>
replaceImportRow (currentRows, mergedRow))
@@ -974,7 +976,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
const result = await apiPost<{ results: BulkApiRow[] }>(
'/posts/bulk',
buildBulkFormData ([target]))
buildBulkFormData ([target]), { locale })
if (result.results.length !== 1)
{
updateRows (currentRows => replaceImportRow (currentRows, originalRow))
@@ -1018,7 +1020,7 @@ const PostImportReviewPage: FC<Props> = ({ user }) => {
{
const result = await apiPost<{ results: BulkApiRow[] }>(
'/posts/bulk',
buildBulkFormData (latestProcessingRows))
buildBulkFormData (latestProcessingRows), { locale })
if (result.results.length !== latestProcessingRows.length)
{
toast ({ title: '登録結果が不完全でした' })
+3 -2
ファイルの表示
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom'
import { useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
@@ -40,7 +41,7 @@ const SOURCE_ISSUES_ID = 'post-import-source-issues'
const PostImportSourcePage: FC<Props> = ({ user }) => {
const editable = canEditContent (user)
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const [source, setSource] = useState ('')
const [loading, setLoading] = useState (false)
const [sourceIssues, setSourceIssues] = useState<ReturnType<typeof validateImportSource>> ([])
+8 -6
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PostList from '@/components/PostList'
import PrefetchLink from '@/components/PrefetchLink'
import TagSidebar from '@/components/TagSidebar'
@@ -33,12 +34,13 @@ const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
const PostListPage: FC = () => {
const locale = useLocale ()
const containerRef = useRef<HTMLDivElement | null> (null)
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = new URLSearchParams (location.search)
const tagsQuery = query.get ('tags') ?? ''
const anyFlg = query.get ('match') === 'any'
@@ -59,8 +61,8 @@ const PostListPage: FC = () => {
createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '',
order } as const
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const posts = data?.posts ?? []
const cursor = ''
const totalPages = data ? Math.ceil (data.count / limit) : 0
@@ -111,14 +113,14 @@ const PostListPage: FC = () => {
try
{
const tagName = tags[0]
setWikiPage (await fetchWikiPageByTitle (tagName, { }))
setWikiPage (await fetchWikiPageByTitle (tagName, { }, locale))
}
catch
{
;
}
}) ()
}, [location.search, tags])
}, [location.search, tags, locale])
return (
<div
+6 -4
ファイルの表示
@@ -2,8 +2,9 @@ import { useQuery } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink'
@@ -70,6 +71,7 @@ const parseOrder = (value: string | null): FetchPostsOrder | null => {
const PostSearchPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const thumbnailTransition = clientAnimationTransition (
@@ -79,7 +81,7 @@ const PostSearchPage: FC = () => {
)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search),
[location.search])
@@ -130,8 +132,8 @@ const PostSearchPage: FC = () => {
updatedTo: qUpdatedTo,
order }
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const results = data?.posts ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+7 -5
ファイルの表示
@@ -2,8 +2,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, LoaderCircle, Pencil, X } from 'lucide-react'
import { Fragment, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import SortHeader from '@/components/SortHeader'
import FieldError from '@/components/common/FieldError'
@@ -40,11 +41,12 @@ const setIf = (qs: URLSearchParams, key: string, value: string) => {
const NicoTagListPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const dialogue = useDialogue ()
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const queryClient = useQueryClient ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
@@ -66,8 +68,8 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
const keys = {
name: qName, linkedTag: qLinkedTag, linkStatus: qLinkStatus, page, limit, order }
const { data, isError, isLoading: loading } = useQuery ({
queryKey: tagsKeys.nicoIndex (keys),
queryFn: () => fetchNicoTags (keys) })
queryKey: tagsKeys.nicoIndex (keys, locale),
queryFn: () => fetchNicoTags (keys, locale) })
const nicoTags = data?.tags ?? []
const count = data?.count ?? 0
@@ -128,7 +130,7 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
try
{
await apiPut<Tag[]> (`/tags/nico/${ id }`, formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
setErrorsByTagId (errors => ({ ...errors, [id]: [] }))
setEditingId (null)
await queryClient.invalidateQueries ({ queryKey: tagsKeys.nicoRoot })
+5 -3
ファイルの表示
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
@@ -28,13 +29,14 @@ type TagFormField =
const TagDetailPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const tagId = String (id ?? '')
const tagKey = tagsKeys.show (tagId)
const tagKey = tagsKeys.show (tagId, locale)
const { data: tag, isLoading: loading } = useQuery ({
queryKey: tagKey,
queryFn: () => fetchTag (tagId) })
queryFn: () => fetchTag (tagId, locale) })
const [name, setName] = useState ('')
const [category, setCategory] = useState<Category> ('general')
@@ -60,7 +62,7 @@ const TagDetailPage: FC = () => {
try
{
const data = await apiPut<Tag> (`/tags/${ id }`, formData)
const data = await apiPut<Tag> (`/tags/${ id }`, formData, { locale })
setName (data.name)
setCategory (data.category as Category)
+5 -3
ファイルの表示
@@ -3,6 +3,7 @@ import { useEffect } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
@@ -45,6 +46,7 @@ const renderStateDiff = (diff: { current: string | null; prev: string | null })
const TagHistoryPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
@@ -54,8 +56,8 @@ const TagHistoryPage: FC = () => {
const limit = Number (query.get ('limit') ?? 20)
const { data, isLoading: loading } = useQuery ({
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }) })
queryKey: tagsKeys.changes ({ ...(id && { id }), page, limit }, locale),
queryFn: () => fetchTagChanges ({ ...(id && { id }), page, limit }, locale) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
@@ -210,7 +212,7 @@ const TagHistoryPage: FC = () => {
change.parentTags
.filter (t => t.type !== 'removed')
.map (t => t.tag.name)
.join (' ') })
.join (' ') }, { locale })
qc.invalidateQueries ({ queryKey: postsKeys.root })
qc.invalidateQueries ({ queryKey: tagsKeys.root })
+6 -4
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import SortHeader from '@/components/SortHeader'
import TagLink from '@/components/TagLink'
@@ -73,11 +74,12 @@ const parseOrder = (value: string | null): FetchTagsOrder | null => {
const TagListPage: FC = () => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const query = useMemo (() => new URLSearchParams (location.search), [location.search])
@@ -129,8 +131,8 @@ const TagListPage: FC = () => {
updatedTo: qUpdatedTo,
deprecated: qDeprecated }
const { data, isLoading: loading } = useQuery ({
queryKey: tagsKeys.index (keys),
queryFn: () => fetchTags (keys) })
queryKey: tagsKeys.index (keys, locale),
queryFn: () => fetchTags (keys, locale) })
const results = data?.tags ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+20 -18
ファイルの表示
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import ErrorScreen from '@/components/ErrorScreen'
import PostEditForm from '@/components/PostEditForm'
import PostEmbed from '@/components/PostEmbed'
@@ -215,6 +216,7 @@ type Props = { user: User | null }
const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const locale = useLocale ()
const behaviourSettings = useClientBehaviourSettings ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
@@ -286,16 +288,16 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return
setProgrammes (await apiGet<TheatreProgramme[]> (
`/theatres/${ id }/programmes`, { params: { limit: '100' } }))
}, [id])
`/theatres/${ id }/programmes`, { locale, params: { limit: '100' } }))
}, [id, locale])
const refreshWeights = useCallback (async () => {
if (!(id))
return
setWeights (await apiGet<TheatrePostSelectionWeights> (
`/theatres/${ id }/post_selection_weights`))
}, [id])
`/theatres/${ id }/post_selection_weights`, { locale }))
}, [id, locale])
const advancePost = useCallback (async () => {
if (!(id))
@@ -304,7 +306,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
setLoading (true)
try
{
await apiPatch<void> (`/theatres/${ id }/next_post`)
await apiPatch<void> (`/theatres/${ id }/next_post`, { }, { locale })
await refreshProgrammes ()
await refreshWeights ()
}
@@ -316,7 +318,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
{
setLoading (false)
}
}, [id, refreshProgrammes, refreshWeights])
}, [id, refreshProgrammes, refreshWeights, locale])
useEffect (() => {
loadingRef.current = loading
@@ -365,7 +367,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
void (async () => {
try
{
const data = await apiGet<Theatre> (`/theatres/${ id }`)
const data = await apiGet<Theatre> (`/theatres/${ id }`, { locale })
if (!(cancelled))
setTheatre (data)
}
@@ -381,7 +383,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return () => {
cancelled = true
}
}, [id, refreshProgrammes, refreshWeights])
}, [id, refreshProgrammes, refreshWeights, locale])
useEffect (() => {
if (!(id))
@@ -400,7 +402,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
{
const newComments = await apiGet<TheatreComment[]> (
`/theatres/${ id }/comments`,
{ params: { no_gt: lastCommentNoRef.current, limit: '20' } })
{ locale, params: { no_gt: lastCommentNoRef.current, limit: '20' } })
if (!(cancelled) && newComments.length > 0)
{
@@ -433,7 +435,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
}
const watchingRequestedAt = performance.now ()
const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`)
const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`, { }, { locale })
if (!(cancelled) && watchingRequestedAt >= theatreInfoReceivedAtRef.current)
applyTheatreInfo (nextInfo)
}
@@ -454,7 +456,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
cancelled = true
clearInterval (interval)
}
}, [applyTheatreInfo, currentPostElapsedMs, id])
}, [applyTheatreInfo, currentPostElapsedMs, id, locale])
useEffect (() => {
if (!(id) || !(theatreInfo.hostFlg) || loadingRef.current || theatreInfo.postId != null)
@@ -487,7 +489,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
void (async () => {
try
{
const nextPost = await fetchPost (String (theatreInfo.postId))
const nextPost = await fetchPost (String (theatreInfo.postId), locale)
if (!(cancelled))
setPost (nextPost)
}
@@ -500,7 +502,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
return () => {
cancelled = true
}
}, [theatreInfo.postId])
}, [theatreInfo.postId, locale])
useEffect (() => {
void refreshProgrammes ()
@@ -570,9 +572,9 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const nextInfo =
theatreInfo.skipVote.voted
? await apiDelete<TheatreInfo> (
`/theatres/${ id }/skip_vote`, { params: { post_id: post.id } })
`/theatres/${ id }/skip_vote`, { locale, params: { post_id: post.id } })
: await apiPut<TheatreInfo> (
`/theatres/${ id }/skip_vote`, { post_id: post.id })
`/theatres/${ id }/skip_vote`, { post_id: post.id }, { locale })
applyTheatreInfo (nextInfo)
@@ -586,7 +588,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
catch (error)
{
if (isApiError (error) && error.response?.status === 409)
applyTheatreInfo (await apiPut<TheatreInfo> (`/theatres/${ id }/watching`))
applyTheatreInfo (await apiPut<TheatreInfo> (`/theatres/${ id }/watching`, { }, { locale }))
console.error (error)
}
@@ -600,7 +602,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
const handleDelete = async (commentNo: number) => {
try
{
await apiDelete (`/theatres/${ id }/comments/${ commentNo }`)
await apiDelete (`/theatres/${ id }/comments/${ commentNo }`, { locale })
setComments (prev => {
const rtn = [...prev]
const idx = rtn.findIndex (x => x.no === commentNo)
@@ -625,7 +627,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
{
setSending (true)
clearValidationErrors ()
await apiPost (`/theatres/${ id }/comments`, { content })
await apiPost (`/theatres/${ id }/comments`, { content }, { locale })
setContent ('')
commentsRef.current?.scrollTo ({
top: 0,
+7 -6
ファイルの表示
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useBeforeUnload, useLocation, useNavigate } from 'react-router-dom'
import { useBeforeUnload, useLocation } from 'react-router-dom'
import { useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
@@ -251,7 +252,7 @@ const ensureCompleteThemeSlots = (
): UserThemeSlotMap =>
USER_THEME_SLOT_SELECTIONS.reduce<UserThemeSlotMap> (
(complete, selection) => {
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
complete[selection] =
themeSlots[selection]
?? getDefaultUserThemeSlot (
@@ -267,7 +268,7 @@ const comparableThemeTokensForSelection = (
themeSlots: UserThemeSlotMap,
selection: UserThemeSlotSelection,
): ThemeTokens => {
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
const fallback =
getDefaultUserThemeSlot (
baseTheme as BuiltinThemeId,
@@ -647,7 +648,7 @@ const ThemeSection: FC<ThemeSectionProps> = (
const SettingPage: FC<Props> = ({ user, setUser }) => {
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const dialogue = useDialogue ()
const { registerUnsavedChangesSource } = useUnsavedChangesGuard ()
@@ -811,7 +812,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
const discardThemeChanges = () => {
const selection = displayedThemeSelection.selection
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
const nextThemeSlots = {
...draftThemeSlots,
[selection]: savedThemeSlots[selection]
@@ -894,7 +895,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
return
const selection = displayedThemeSelection.selection
const [_, baseTheme, slotNo] = selection.split (':')
const [, baseTheme, slotNo] = selection.split (':')
themeSlots = {
...baseState.themeSlots,
[selection]: savedThemeSlots[selection]
+12 -10
ファイルの表示
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useLocation, useParams } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import PostList from '@/components/PostList'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
@@ -24,11 +25,12 @@ import type { Tag } from '@/types'
const WikiDetailPage: FC = () => {
const locale = useLocale ()
const params = useParams ()
const title = params.title ?? ''
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const defaultTag = useMemo (() => ({ name: title, category: 'general' } as Tag), [title])
@@ -37,8 +39,8 @@ const WikiDetailPage: FC = () => {
const { data: wikiPage, isLoading: loading, isSuccess, isFetching, error } = useQuery ({
enabled: Boolean (title) && !(/^\d+$/.test (title)),
queryKey: wikiKeys.show (title, { version }),
queryFn: () => fetchWikiPageByTitle (title, { version }) })
queryKey: wikiKeys.show (title, { version }, locale),
queryFn: () => fetchWikiPageByTitle (title, { version }, locale) })
const missingWiki = isSuccess && !(isFetching) && wikiPage == null
const { data: titleIndex, isPending: lookupPending,
@@ -63,8 +65,8 @@ const WikiDetailPage: FC = () => {
const { data: tag, isPending: tagPending,
isFetching: tagFetching, error: tagError } = useQuery ({
enabled: Boolean (effectiveTitle),
queryKey: tagsKeys.show (effectiveTitle),
queryFn: () => fetchTagByName (effectiveTitle) })
queryKey: tagsKeys.show (effectiveTitle, locale),
queryFn: () => fetchTagByName (effectiveTitle, locale) })
const keys = {
tags: effectiveTitle, match: 'all', page: 1, limit: 8, url: '', title: '',
@@ -73,8 +75,8 @@ const WikiDetailPage: FC = () => {
order: 'original_created_at:desc' } as const
const { data } = useQuery ({
enabled: Boolean (effectiveTitle) && !(version),
queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) })
queryKey: postsKeys.index (keys, locale),
queryFn: () => fetchPosts (keys, locale) })
const posts = data?.posts || []
useEffect (() => {
@@ -97,7 +99,7 @@ const WikiDetailPage: FC = () => {
void (async () => {
try
{
const data = await fetchWikiPage (title, { })
const data = await fetchWikiPage (title, { }, locale)
if (active)
navigate (`/wiki/${ encodeURIComponent (data.title) }`, { replace: true })
}
@@ -110,7 +112,7 @@ const WikiDetailPage: FC = () => {
return () => {
active = false
}
}, [title, navigate])
}, [title, navigate, locale])
const displayError = error ?? (missingWiki ? lookupError ?? tagError : null)
if (displayError != null)
+4 -2
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useParams } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
@@ -14,6 +15,7 @@ import type { WikiPageDiff } from '@/types'
const WikiDiffPage: FC = () => {
const locale = useLocale ()
const { id } = useParams ()
const location = useLocation ()
@@ -29,9 +31,9 @@ const WikiDiffPage: FC = () => {
useEffect (() => {
void (async () => {
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { params: { from, to } }))
setDiff (await apiGet<WikiPageDiff> (`/wiki/${ id }/diff`, { locale, params: { from, to } }))
}) ()
}, [from, id, to])
}, [from, id, to, locale])
return (
<MainArea>
+8 -6
ファイルの表示
@@ -3,8 +3,9 @@ import MarkdownIt from 'markdown-it'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import MdEditor from 'react-markdown-editor-lite'
import { useParams, useNavigate } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
@@ -36,11 +37,12 @@ type WikiFormField = 'title' | 'body'
const WikiEditPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const { id } = useParams ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const qc = useQueryClient ()
@@ -60,8 +62,8 @@ const WikiEditPage: FC<Props> = ({ user }) => {
try
{
await apiPut (`/wiki/${ id }`, formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }),
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
qc.setQueryData (wikiKeys.show (title, { }, locale),
(prev: WikiPage) => ({ ...prev, title, body }))
qc.invalidateQueries ({ queryKey: wikiKeys.root })
toast ({ title: '投稿成功!' })
@@ -80,12 +82,12 @@ const WikiEditPage: FC<Props> = ({ user }) => {
void (async () => {
setLoading (true)
const data = await apiGet<WikiPage> (`/wiki/${ id }`)
const data = await apiGet<WikiPage> (`/wiki/${ id }`, { locale })
setTitle (data.title)
setBody (data.body)
setLoading (false)
}) ()
}, [editable, id])
}, [editable, id, locale])
if (!(editable))
return <Forbidden/>
+4 -2
ファイルの表示
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation } from 'react-router-dom'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
@@ -15,6 +16,7 @@ import type { WikiPageChange } from '@/types'
const WikiHistoryPage: FC = () => {
const locale = useLocale ()
const [changes, setChanges] = useState<WikiPageChange[]> ([])
const location = useLocation ()
@@ -23,9 +25,9 @@ const WikiHistoryPage: FC = () => {
useEffect (() => {
void (async () => {
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { params: id ? { id } : { } }))
setChanges (await apiGet<WikiPageChange[]> ('/wiki/changes', { locale, params: id ? { id } : { } }))
}) ()
}, [id, location.search])
}, [id, location.search, locale])
return (
<MainArea>
+6 -4
ファイルの表示
@@ -3,9 +3,10 @@ import MarkdownIt from 'markdown-it'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import MdEditor from 'react-markdown-editor-lite'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation } from 'react-router-dom'
import 'react-markdown-editor-lite/lib/index.css'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
@@ -35,10 +36,11 @@ type WikiFormField = 'title' | 'body'
const WikiNewPage: FC<Props> = ({ user }) => {
const locale = useLocale ()
const editable = canEditContent (user)
const location = useLocation ()
const navigate = useNavigate ()
const navigate = useLocaleNavigate ()
const qc = useQueryClient ()
const query = new URLSearchParams (location.search)
@@ -59,9 +61,9 @@ const WikiNewPage: FC<Props> = ({ user }) => {
try
{
const data = await apiPost<WikiPage> ('/wiki', formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
{ locale, headers: { 'Content-Type': 'multipart/form-data' } })
await qc.cancelQueries ({ queryKey: wikiKeys.root })
qc.setQueryData (wikiKeys.show (data.title, { }), data)
qc.setQueryData (wikiKeys.show (data.title, { }, locale), data)
await Promise.all ([
qc.invalidateQueries ({ queryKey: wikiKeys.root }),
qc.invalidateQueries ({ queryKey: tagsKeys.root })])
+5 -3
ファイルの表示
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import PrefetchLink from '@/components/PrefetchLink'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
@@ -15,12 +16,13 @@ import type { WikiPage } from '@/types'
const WikiSearchPage: FC = () => {
const locale = useLocale ()
const [title, setTitle] = useState ('')
const [text, setText] = useState ('')
const [results, setResults] = useState<WikiPage[]> ([])
const search = async () => {
setResults (await apiGet ('/wiki', { params: { title } }))
setResults (await apiGet ('/wiki', { locale, params: { title } }))
}
const handleSearch = (ev: FormEvent) => {
@@ -30,9 +32,9 @@ const WikiSearchPage: FC = () => {
useEffect (() => {
void (async () => {
setResults (await apiGet ('/wiki', { params: { title: '' } }))
setResults (await apiGet ('/wiki', { locale, params: { title: '' } }))
}) ()
}, [])
}, [locale])
return (
<MainArea>
+1 -1
ファイルの表示
@@ -16,7 +16,7 @@ if (typeof originalTransform === 'function')
function (code, id)
{
return (/[?&](raw|url)(?:&|$)/.test (id)
? null
? Promise.resolve (undefined)
: originalTransform.call (this, code, id))
}
}