Add languages and entity_translations tables to database schema Implement localized queries and translation management in backend Integrate frontend i18n and add translation UI components
335 lines
14 KiB
SQL
335 lines
14 KiB
SQL
CREATE TABLE IF NOT EXISTS environments (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS languages (
|
|
code text PRIMARY KEY,
|
|
name text NOT NULL,
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
is_default boolean NOT NULL DEFAULT false,
|
|
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
|
|
CHECK (code ~ '^[a-z]{2}(-[A-Z]{2})?$'),
|
|
CHECK (length(name) BETWEEN 1 AND 80)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS languages_single_default_idx
|
|
ON languages (is_default)
|
|
WHERE is_default = true;
|
|
|
|
INSERT INTO languages (code, name, enabled, is_default, sort_order)
|
|
VALUES
|
|
('en', 'English', true, true, 10),
|
|
('zh-CN', '简体中文', true, false, 20)
|
|
ON CONFLICT (code) DO NOTHING;
|
|
|
|
CREATE TABLE IF NOT EXISTS entity_translations (
|
|
entity_type text NOT NULL CHECK (
|
|
entity_type IN (
|
|
'pokemon',
|
|
'skills',
|
|
'environments',
|
|
'favorite-things',
|
|
'item-categories',
|
|
'item-usages',
|
|
'acquisition-methods',
|
|
'items',
|
|
'maps',
|
|
'habitats',
|
|
'daily-checklist-items'
|
|
)
|
|
),
|
|
entity_id integer NOT NULL,
|
|
locale text NOT NULL REFERENCES languages(code) ON DELETE CASCADE,
|
|
field_name text NOT NULL CHECK (field_name IN ('name', 'title')),
|
|
value text NOT NULL,
|
|
PRIMARY KEY (entity_type, entity_id, locale, field_name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS entity_translations_lookup_idx
|
|
ON entity_translations (entity_type, entity_id, field_name, locale);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
email text NOT NULL UNIQUE,
|
|
display_name text NOT NULL,
|
|
password_hash text NOT NULL,
|
|
email_verified_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CHECK (email = lower(email)),
|
|
CHECK (length(display_name) BETWEEN 1 AND 40)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS email_verification_tokens (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash text NOT NULL UNIQUE,
|
|
expires_at timestamptz NOT NULL,
|
|
used_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS email_verification_tokens_user_id_idx
|
|
ON email_verification_tokens(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash text NOT NULL UNIQUE,
|
|
expires_at timestamptz NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS user_sessions_user_id_idx
|
|
ON user_sessions(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS daily_checklist_items (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
title text NOT NULL,
|
|
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
|
|
created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
|
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS daily_checklist_items_sort_order_idx
|
|
ON daily_checklist_items(sort_order, id);
|
|
|
|
CREATE TABLE IF NOT EXISTS skills (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE,
|
|
has_item_drop boolean NOT NULL DEFAULT false
|
|
);
|
|
|
|
ALTER TABLE skills DROP COLUMN IF EXISTS subcategory;
|
|
ALTER TABLE skills ADD COLUMN IF NOT EXISTS has_item_drop boolean NOT NULL DEFAULT false;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS skills_name_key ON skills(name);
|
|
|
|
CREATE TABLE IF NOT EXISTS favorite_things (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pokemon (
|
|
id integer PRIMARY KEY,
|
|
name text NOT NULL UNIQUE,
|
|
environment_id integer NOT NULL REFERENCES environments(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pokemon_skills (
|
|
pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
|
|
skill_id integer NOT NULL REFERENCES skills(id),
|
|
PRIMARY KEY (pokemon_id, skill_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pokemon_favorite_things (
|
|
pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
|
|
favorite_thing_id integer NOT NULL REFERENCES favorite_things(id),
|
|
PRIMARY KEY (pokemon_id, favorite_thing_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS item_categories (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS item_usages (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS acquisition_methods (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS items (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE,
|
|
category_id integer NOT NULL REFERENCES item_categories(id),
|
|
usage_id integer REFERENCES item_usages(id),
|
|
dyeable boolean NOT NULL DEFAULT false,
|
|
dual_dyeable boolean NOT NULL DEFAULT false,
|
|
pattern_editable boolean NOT NULL DEFAULT false,
|
|
no_recipe boolean NOT NULL DEFAULT false
|
|
);
|
|
|
|
ALTER TABLE items ALTER COLUMN usage_id DROP NOT NULL;
|
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS no_recipe boolean NOT NULL DEFAULT false;
|
|
ALTER TABLE items DROP COLUMN IF EXISTS no_habitat;
|
|
|
|
CREATE TABLE IF NOT EXISTS recipes (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
item_id integer NOT NULL UNIQUE REFERENCES items(id)
|
|
);
|
|
|
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS item_id integer REFERENCES items(id);
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'items'
|
|
AND column_name = 'recipe_id'
|
|
) THEN
|
|
EXECUTE '
|
|
UPDATE recipes r
|
|
SET item_id = linked.item_id
|
|
FROM (
|
|
SELECT DISTINCT ON (recipe_id) recipe_id, id AS item_id
|
|
FROM items
|
|
WHERE recipe_id IS NOT NULL
|
|
ORDER BY recipe_id, id
|
|
) linked
|
|
WHERE r.id = linked.recipe_id
|
|
AND r.item_id IS NULL
|
|
';
|
|
END IF;
|
|
END $$;
|
|
|
|
DELETE FROM recipes WHERE item_id IS NULL;
|
|
|
|
ALTER TABLE recipes ALTER COLUMN item_id SET NOT NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS recipes_item_id_key ON recipes(item_id);
|
|
ALTER TABLE recipes DROP COLUMN IF EXISTS name;
|
|
ALTER TABLE items DROP COLUMN IF EXISTS recipe_id;
|
|
|
|
CREATE TABLE IF NOT EXISTS recipe_acquisition_methods (
|
|
recipe_id integer NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
|
acquisition_method_id integer NOT NULL REFERENCES acquisition_methods(id),
|
|
PRIMARY KEY (recipe_id, acquisition_method_id)
|
|
);
|
|
|
|
DROP TABLE IF EXISTS item_item_tags;
|
|
DROP TABLE IF EXISTS item_tags;
|
|
|
|
CREATE TABLE IF NOT EXISTS item_acquisition_methods (
|
|
item_id integer NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
|
acquisition_method_id integer NOT NULL REFERENCES acquisition_methods(id),
|
|
PRIMARY KEY (item_id, acquisition_method_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS item_favorite_things (
|
|
item_id integer NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
|
favorite_thing_id integer NOT NULL REFERENCES favorite_things(id),
|
|
PRIMARY KEY (item_id, favorite_thing_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pokemon_skill_item_drops (
|
|
pokemon_id integer NOT NULL,
|
|
skill_id integer NOT NULL,
|
|
item_id integer NOT NULL REFERENCES items(id),
|
|
PRIMARY KEY (pokemon_id, skill_id),
|
|
FOREIGN KEY (pokemon_id, skill_id) REFERENCES pokemon_skills(pokemon_id, skill_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS recipe_materials (
|
|
recipe_id integer NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
|
item_id integer NOT NULL REFERENCES items(id),
|
|
quantity integer NOT NULL CHECK (quantity > 0),
|
|
PRIMARY KEY (recipe_id, item_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS maps (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS habitats (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name text NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS habitat_recipe_items (
|
|
habitat_id integer NOT NULL REFERENCES habitats(id) ON DELETE CASCADE,
|
|
item_id integer NOT NULL REFERENCES items(id),
|
|
quantity integer NOT NULL CHECK (quantity > 0),
|
|
PRIMARY KEY (habitat_id, item_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS habitat_pokemon (
|
|
habitat_id integer NOT NULL REFERENCES habitats(id) ON DELETE CASCADE,
|
|
pokemon_id integer NOT NULL REFERENCES pokemon(id) ON DELETE CASCADE,
|
|
map_id integer NOT NULL REFERENCES maps(id),
|
|
time_of_day text NOT NULL CHECK (time_of_day IN ('早晨', '中午', '傍晚', '晚上')),
|
|
weather text NOT NULL CHECK (weather IN ('晴天', '阴天', '雨天')),
|
|
rarity integer NOT NULL CHECK (rarity BETWEEN 1 AND 3),
|
|
PRIMARY KEY (habitat_id, pokemon_id, map_id, time_of_day, weather)
|
|
);
|
|
|
|
ALTER TABLE environments ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE environments ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE environments ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE environments ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE skills ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE skills ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE skills ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE skills ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE favorite_things ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE pokemon ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE item_categories ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE item_usages ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE acquisition_methods ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE maps ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE maps ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE maps ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE maps ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
ALTER TABLE habitats ADD COLUMN IF NOT EXISTS created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE habitats ADD COLUMN IF NOT EXISTS updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL;
|
|
ALTER TABLE habitats ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
|
|
ALTER TABLE habitats ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
CREATE TABLE IF NOT EXISTS wiki_edit_logs (
|
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
entity_type text NOT NULL,
|
|
entity_id integer NOT NULL,
|
|
action text NOT NULL CHECK (action IN ('create', 'update', 'delete')),
|
|
user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
|
changes jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE wiki_edit_logs ADD COLUMN IF NOT EXISTS changes jsonb NOT NULL DEFAULT '[]'::jsonb;
|
|
|
|
CREATE INDEX IF NOT EXISTS wiki_edit_logs_entity_idx
|
|
ON wiki_edit_logs(entity_type, entity_id, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS wiki_edit_logs_user_id_idx
|
|
ON wiki_edit_logs(user_id);
|