多言語化の基盤作り (#55) (#421)

Reviewed-on: #421
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #421 でマージされました。
このコミットが含まれているのは:
2026-10-03 02:00:09 +09:00
committed by みてるぞ
コミット e6ab941774
91個のファイルの変更、3370行の追加、1223行の削除
+20
ファイルの表示
@@ -0,0 +1,20 @@
class CreateLanguages < ActiveRecord::Migration[8.0]
def up
create_table :languages, id: { type: :string, limit: 16 }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
end
execute <<~SQL
INSERT INTO
languages(code, name, created_at)
VALUES
('ja', '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :languages
end
end
+23
ファイルの表示
@@ -0,0 +1,23 @@
class CreateScripts < ActiveRecord::Migration[8.0]
def up
create_table :scripts, id: { type: 'CHAR(4)' }, primary_key: :code do |t|
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
t.check_constraint "REGEXP_LIKE(code, '^[A-Z][a-z]{3}$', 'c')",
name: 'scripts_code_check'
end
execute <<~SQL
INSERT INTO
scripts(code, name, created_at)
VALUES
('Jpan', '漢字、仮名文字およびラテン文字', #{ connection.quote Time.current })
SQL
end
def down
drop_table :scripts
end
end
+25
ファイルの表示
@@ -0,0 +1,25 @@
class CreateRegions < ActiveRecord::Migration[8.0]
def up
create_table :regions, id: { type: :integer, limit: 2 }, primary_key: :code do |t|
t.column :alpha_code, 'CHAR(2)'
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
t.check_constraint "REGEXP_LIKE(alpha_code, '^[A-Z]{2}$', 'c')",
name: 'regions_alpha_code_check'
t.index [:alpha_code], unique: true
end
execute <<~SQL
INSERT INTO
regions(code, alpha_code, name, created_at)
VALUES
(392, 'JP', '日本国', #{ connection.quote Time.current })
SQL
end
def down
drop_table :regions
end
end
+38
ファイルの表示
@@ -0,0 +1,38 @@
class CreateLocales < ActiveRecord::Migration[8.0]
def up
create_table :locales, id: { type: :string, limit: 32 }, primary_key: :code do |t|
t.string :language_code, limit: 16, null: false, index: true
t.column :script_code, 'CHAR(4)', null: false
t.integer :region_code, limit: 2, null: false
t.string :variants, limit: 255, null: false, default: ''
t.string :name, null: false
t.datetime :deprecated_at, index: true
t.datetime :created_at, null: false
t.foreign_key :languages, column: :language_code, primary_key: :code
t.foreign_key :scripts, column: :script_code, primary_key: :code
t.foreign_key :regions, column: :region_code, primary_key: :code
t.check_constraint ('REGEXP_LIKE(variants, ' +
"'^(" +
'(([a-z0-9]{5,8}|[0-9][a-z0-9]{3})' +
'(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*' +
'(-x(-[a-z0-9]{1,8})+)?)' +
'|(x(-[a-z0-9]{1,8})+)' +
")?$', 'c')"),
name: 'locales_variants_check'
t.index [:language_code, :script_code, :region_code, :variants], unique: true
end
execute <<~SQL
INSERT INTO
locales(code, language_code, script_code, region_code, name, created_at)
VALUES
('ja', 'ja', 'Jpan', 392, '日本語', #{ connection.quote Time.current })
SQL
end
def down
drop_table :locales
end
end
+69
ファイルの表示
@@ -0,0 +1,69 @@
class AddColumnsToTagNames < ActiveRecord::Migration[8.0]
def up
add_reference :tag_names, :tag, after: :id, foreign_key: true
add_column :tag_names, :language_code, :string,
limit: 16, null: false, after: :tag_id, default: 'ja'
add_column :tag_names, :script_code, 'CHAR(4)',
null: false, after: :name, default: 'Jpan'
add_column :tag_names, :primary_flg, :boolean,
null: false, after: :script_code, default: true
add_column :tag_names, :auto_generated, :boolean,
null: false, default: false, after: :primary_flg
add_foreign_key :tag_names, :languages, column: :language_code, primary_key: :code
add_foreign_key :tag_names, :scripts, column: :script_code, primary_key: :code
remove_index :tag_names, :name
add_index :tag_names, [:language_code, :name], unique: true
change_column_default :tag_names, :language_code, from: 'ja', to: nil
change_column_default :tag_names, :script_code, from: 'Jpan', to: nil
execute <<~SQL
UPDATE
tag_names tn
LEFT JOIN
tags AS t
ON
t.tag_name_id = COALESCE(tn.canonical_id, tn.id)
SET
tn.tag_id = t.id
, tn.primary_flg = CASE
WHEN tn.canonical_id IS NULL THEN
1
ELSE
0
END
SQL
change_column_default :tag_names, :primary_flg, from: true, to: nil
add_column :tag_names, :primary_tag_id, :bigint,
as: 'CASE WHEN primary_flg THEN tag_id ELSE NULL END'
add_index :tag_names, [:primary_tag_id, :language_code], unique: true
add_check_constraint :tag_names,
'primary_flg OR (NOT auto_generated)',
name: 'chk_tag_names_auto_generated_primary'
end
def down
remove_check_constraint :tag_names,
name: 'chk_tag_names_auto_generated_primary'
remove_index :tag_names, [:primary_tag_id, :language_code]
remove_column :tag_names, :primary_tag_id
remove_foreign_key :tag_names, column: :language_code
remove_foreign_key :tag_names, column: :script_code
remove_index :tag_names, [:language_code, :name]
add_index :tag_names, :name, unique: true
remove_column :tag_names, :auto_generated
remove_column :tag_names, :primary_flg
remove_column :tag_names, :script_code
remove_column :tag_names, :language_code
remove_reference :tag_names, :tag, foreign_key: true
end
end
+68
ファイルの表示
@@ -0,0 +1,68 @@
class AddNamesToTagVersions < ActiveRecord::Migration[8.0]
class TagVersion < ActiveRecord::Base
self.table_name = 'tag_versions'
end
def up
add_column :tag_versions, :names, :json
TagVersion.reset_column_information
say_with_time 'Backfilling multilingual tag version names' do
TagVersion.find_each do |version|
aliases = version.aliases.to_s.split.filter_map do |name|
name.present? && { name:, script_code: 'Jpan' }
end
names = { ja: { name: version.name,
script_code: 'Jpan',
auto_generated: false,
aliases: } }
version.update_columns(names:)
end
end
add_names_constraint!
change_column_null :tag_versions, :names, false
end
def down
remove_check_constraint :tag_versions,
name: 'chk_tag_versions_names_schema'
remove_column :tag_versions, :names
end
private
def add_names_constraint!
schema = { type: 'object',
minProperties: 1,
patternProperties: { '^.{1,16}$': name_schema },
additionalProperties: false }
quoted_schema = connection.quote(JSON.generate(schema))
add_check_constraint :tag_versions,
"JSON_SCHEMA_VALID(#{ quoted_schema }, names)",
name: 'chk_tag_versions_names_schema'
end
def name_schema
{ type: 'object',
required: ['name', 'script_code', 'auto_generated', 'aliases'],
properties: { **name_schema_base,
auto_generated: { type: 'boolean' },
aliases: { type: 'array',
uniqueItems: true,
items: { type: 'object',
required: ['name', 'script_code'],
properties: name_schema_base,
additionalProperties: false } } },
additionalProperties: false }
end
def name_schema_base
{ name: { type: 'string', minLength: 1, maxLength: 255 },
script_code: { type: 'string', pattern: '^[A-Z][a-z]{3}$' } }
end
end
生成ファイル
+61 -2
ファイルの表示
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@@ -138,6 +138,29 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
end
create_table "languages", primary_key: "code", id: { type: :string, limit: 16 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_languages_on_deprecated_at"
end
create_table "locales", primary_key: "code", id: { type: :string, limit: 32 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "language_code", limit: 16, null: false
t.string "script_code", limit: 4, null: false
t.integer "region_code", limit: 2, null: false
t.string "variants", default: "", null: false
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_locales_on_deprecated_at"
t.index ["language_code", "script_code", "region_code", "variants"], name: "idx_on_language_code_script_code_region_code_varian_b0ba85c39a", unique: true
t.index ["language_code"], name: "index_locales_on_language_code"
t.index ["region_code"], name: "fk_rails_175159992c"
t.index ["script_code"], name: "fk_rails_0b74ce96a8"
t.check_constraint "regexp_like(`variants`,_utf8mb4'^((([a-z0-9]{5,8}|[0-9][a-z0-9]{3})(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(-x(-[a-z0-9]{1,8})+)?)|(x(-[a-z0-9]{1,8})+))?$',_utf8mb4'c')", name: "locales_variants_check"
end
create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "material_id", null: false
t.string "profile", default: "legacy_drive", null: false
@@ -388,6 +411,24 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
end
create_table "regions", primary_key: "code", id: { type: :integer, limit: 2 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "alpha_code", limit: 2
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["alpha_code"], name: "index_regions_on_alpha_code", unique: true
t.index ["deprecated_at"], name: "index_regions_on_deprecated_at"
t.check_constraint "regexp_like(`alpha_code`,_utf8mb4'^[A-Z]{2}$',_utf8mb4'c')", name: "regions_alpha_code_check"
end
create_table "scripts", primary_key: "code", id: { type: :string, limit: 4 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_scripts_on_deprecated_at"
t.check_constraint "regexp_like(`code`,_utf8mb4'^[A-Z][a-z]{3}$',_utf8mb4'c')", name: "scripts_code_check"
end
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "user_id", null: false
t.datetime "created_at", null: false
@@ -420,12 +461,22 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
end
create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "tag_id"
t.string "language_code", limit: 16, null: false
t.string "name", null: false
t.string "script_code", limit: 4, null: false
t.boolean "primary_flg", null: false
t.boolean "auto_generated", default: false, null: false
t.bigint "canonical_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)"
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
t.index ["name"], name: "index_tag_names_on_name", unique: true
t.index ["language_code", "name"], name: "index_tag_names_on_language_code_and_name", unique: true
t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true
t.index ["script_code"], name: "fk_rails_dd783b3d1c"
t.index ["tag_id"], name: "index_tag_names_on_tag_id"
t.check_constraint "(0 <> `primary_flg`) or (0 = `auto_generated`)", name: "chk_tag_names_auto_generated_primary"
end
create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -447,11 +498,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.bigint "created_by_user_id"
t.json "names", null: false
t.index ["created_at"], name: "index_tag_versions_on_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"object\",\"minProperties\":1,\"patternProperties\":{\"^.{1,16}$\":{\"type\":\"object\",\"required\":[\"name\",\"script_code\",\"auto_generated\",\"aliases\"],\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":255},\"script_code\":{\"type\":\"string\",\"pattern\":\"^[A-Z][a-z]{3}$\"},\"auto_generated\":{\"type\":\"boolean\"},\"aliases\":{\"type\":\"array\",\"uniqueItems\":true,\"items\":{\"type\":\"object\",\"required\":[\"name\",\"script_code\"],\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":255},\"script_code\":{\"type\":\"string\",\"pattern\":\"^[A-Z][a-z]{3}$\"}},\"additionalProperties\":false}}},\"additionalProperties\":false}},\"additionalProperties\":false}',`names`)", name: "chk_tag_versions_names_schema"
end
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -701,6 +754,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
add_foreign_key "gekanator_question_suggestions", "users"
add_foreign_key "gekanator_questions", "gekanator_question_suggestions"
add_foreign_key "gekanator_questions", "users", column: "created_by_id"
add_foreign_key "locales", "languages", column: "language_code", primary_key: "code"
add_foreign_key "locales", "regions", column: "region_code", primary_key: "code"
add_foreign_key "locales", "scripts", column: "script_code", primary_key: "code"
add_foreign_key "material_export_items", "materials"
add_foreign_key "material_export_items", "users", column: "created_by_user_id"
add_foreign_key "material_import_blocks", "users", column: "created_by_user_id"
@@ -734,7 +790,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
add_foreign_key "settings", "users"
add_foreign_key "tag_implications", "tags"
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code"
add_foreign_key "tag_names", "scripts", column: "script_code", primary_key: "code"
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
add_foreign_key "tag_names", "tags"
add_foreign_key "tag_similarities", "tags"
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
add_foreign_key "tag_versions", "users", column: "created_by_user_id"