перевод на русский

This commit is contained in:
2026-09-12 18:02:23 +04:00
parent a0464d3388
commit 4d760528cc
4 changed files with 9432 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
-- Generated from ChatGPT WoW localisation source.csv.
-- Apply to dc_world. Existing frFR rows are not modified.
INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`) VALUES
(92718,0,0,'ruRU','Мне понадобится твоя помощь, чтобы остановить Гул''дана. Освободи остальных Иллидари, поскорее!'),
(92980,1,0,'ruRU','Демоны хотят уничтожить наш мир. Мы должны освободить наших союзников.'),
(92986,1,0,'ruRU','Объяснения могут подождать. Нужно убить демонов.'),
(93011,0,0,'ruRU','Вы слышали лорда Иллидана. Давайте найдем этот краеугольный камень.'),
(93011,1,0,'ruRU','С его помощью мы сможем вторгнуться на любую планету Легиона, даже на Аргус.'),
(93105,0,0,'ruRU','Мои глаза… Мои глаза!'),
(93105,2,0,'ruRU','Довольно! Мой адский колосс тебя раздавит.'),
(93117,0,0,'ruRU','Какая боль!'),
(93127,0,0,'ruRU','Их главные цели — Паучья крепость, Двигатель душ и Кузница. Слуги Иллидана должны добиться успеха.'),
(93127,1,0,'ruRU','Если эти эсминцы не будут уничтожены, мы долго не продержимся!'),
(93127,3,0,'ruRU','Я чувствую, как это течет по моим венам. Это преображает мое тело.'),
(93127,4,0,'ruRU','Нет… Маннетрель.'),
(93221,1,0,'ruRU','Вы не переживёте Адскую Вершину…'),
(93759,1,0,'ruRU','Используйте тигель для завершения ритуала.'),
(93802,0,0,'ruRU','Придите, дети мои. Насладитесь нашими врагами.'),
(93802,2,0,'ruRU','Лорд Саргерас, нет!!!'),
(94400,0,0,'ruRU','Я вас всех уничтожу.'),
(94400,1,0,'ruRU','Я убью каждого демона на своём пути.'),
(95226,1,0,'ruRU','Здесь вы — добыча.'),
(95226,3,0,'ruRU','В моей клетке, и даже быстрее.'),
(96277,0,0,'ruRU','Я тебя расчленю.'),
(96279,1,0,'ruRU','В моей клетке, и даже быстрее.'),
(96279,2,0,'ruRU','Так сильно они жаждут попасть в рабство.'),
(96402,1,0,'ruRU','Умри в славе битвы!'),
(96420,0,0,'ruRU','Нас в десять раз больше. Сможем ли мы действительно их победить?'),
(96436,1,0,'ruRU','Они от нас не скроются. Иллидари, атака!'),
(96441,1,0,'ruRU','Да будет проклято твое призрачное видение!'),
(96494,4,0,'ruRU','Вы смеете нападать на нас здесь?!'),
(96652,1,0,'ruRU','Аа ...'),
(97034,0,0,'ruRU','Я живу, чтобы служить.'),
(97034,1,0,'ruRU','Мой клинок пронзит тебя.'),
(97057,0,0,'ruRU','Тебе нравится мой новый меч? Подойди поближе.'),
(97057,1,0,'ruRU','Глупые мелочи…'),
(97058,0,0,'ruRU','Мой рой насытится твоей душой!'),
(97244,0,0,'ruRU','Забирайте краеугольный камень! Давайте направимся к воротам внизу и вернемся в Черный Храм!'),
(97370,1,0,'ruRU','От моего огня не останется ничего, кроме костей.'),
(97459,0,0,'ruRU','Мои силы иссякли. Я подвел тебя.'),
(97459,1,0,'ruRU','Клянусь отдать свою жизнь, если это потребуется.'),
(98229,0,0,'ruRU','Активируйте три портала и соберите остальные силы.'),
(98229,1,0,'ruRU','Я пробьюсь сквозь демонов и встречусь с тобой у вулкана.'),
(98292,0,0,'ruRU','Убейте их всех!'),
(98354,0,0,'ruRU','Я иду с тобой. Пойдём убьём ещё нескольких демонов.'),
(98459,0,0,'ruRU','Знаете, это я должен быть главным.'),
(98483,0,0,'ruRU','Для меня это вкусное, сочное мясо.'),
(98497,1,0,'ruRU','Мне скоро принесут еду.'),
(98497,3,0,'ruRU','Мои дети приготовят для вас идеальное блюдо.'),
(98497,4,0,'ruRU','Победа Легиона неизбежна, дитя моё.'),
(98497,5,0,'ruRU','Я сам доставлю тебя к Тиранне.'),
(98497,6,0,'ruRU','Грязный маленький эльф. Я чувствую вкус скверны на тебе.'),
(98711,0,0,'ruRU','Мать маленьких дьяволят находится в той пещере, прямо там.'),
(98986,0,0,'ruRU','Мои развращающие тайны поглотят тебя!'),
(98986,1,0,'ruRU','Они крадут наши секреты, моя королева…'),
(99045,1,0,'ruRU','Я мог бы в одиночку уничтожить Легион!'),
(99631,0,0,'ruRU','Я что, не сплю? Как это возможно?'),
(99632,0,0,'ruRU','Сколько лет я потратил впустую в этой камере?'),
(99917,1,0,'ruRU','Севис опускает взгляд на сломленного, умирающего мистика.'),
(99917,2,0,'ruRU','Встретимся у последнего пешеходного моста.'),
(100545,0,0,'ruRU','За лорда Иллидана!'),
(103432,0,0,'ruRU','Я тебя расчленю.')
ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);
INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`)
SELECT ct.`CreatureID`,ct.`GroupID`,ct.`ID`,'ruRU',REPLACE(REPLACE(ct.`Text`, 'begins to cast', 'начинает произносить'), '[Carrion Storm]', '[Буря Падали]')
FROM `creature_text` ct WHERE ct.`CreatureID`=97058 AND ct.`GroupID`=1 AND ct.`ID`=0
ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);
INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`)
SELECT ct.`CreatureID`,ct.`GroupID`,ct.`ID`,'ruRU',REPLACE(REPLACE(REPLACE(ct.`Text`, 'Nearby', 'Ближайшие'), '[Spider Eggs]', '[Паучьи яйца]'), 'will hatch soon if not destroyed!.', 'скоро вылупятся, если их не уничтожить!')
FROM `creature_text` ct WHERE ct.`CreatureID`=97059 AND ct.`GroupID`=2 AND ct.`ID`=0
ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);
INSERT INTO `gossip_menu_option_locale` (`MenuId`,`OptionIndex`,`Locale`,`OptionText`,`BoxText`) VALUES
(18776,0,'ruRU','Уайтмун, я возьму одну из твоих пораженных гангреной летучих мышей. Мы найдем то, за чем пришли.','')
ON DUPLICATE KEY UPDATE `OptionText`=VALUES(`OptionText`);
File diff suppressed because it is too large Load Diff
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Build ruRU locale overlays from reviewed French-to-Russian CSV rows."""
from __future__ import annotations
import csv
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE_CSV = ROOT / "tools" / "ChatGPT WoW localisation source.csv"
OUT_DIR = ROOT / "sql" / "sylvania" / "locales"
INVALID = {"", "Загрузка..."}
BROADCAST_ROW = re.compile(
r"^\((\d+),'frFR','((?:''|[^'])*)','((?:''|[^'])*)',(\d+)\)(?:,|;)$"
)
TMP_BROADCAST_ROW = re.compile(r"^\((\d+), '((?:''|[^'])*)'\)(?:,|;)$")
CREATURE_ROW = re.compile(
r"^\((\d+),(\d+),(\d+),'frFR','((?:''|[^'])*)'\)(?:,|;)$"
)
def sql_quote(value: str) -> str:
return "'" + value.replace("'", "''") + "'"
def translations() -> dict[str, str]:
result: dict[str, str] = {}
with SOURCE_CSV.open(encoding="utf-8-sig", newline="") as source:
for row in csv.DictReader(source):
russian = row["russian_text"].strip()
if row["kind"] == "literal" and russian not in INVALID:
result[row["french_text"]] = russian
return result
def build_hotfixes(translated: dict[str, str]) -> int:
rows: dict[int, tuple[str, str, int]] = {}
dynamic_rows: dict[int, str] = {}
source = ROOT / "sql/sylvania/broadcast_text_frFR_client.sql"
for line in source.read_text(encoding="utf-8").splitlines():
match = BROADCAST_ROW.match(line)
if not match:
continue
entry, male, female, build = match.groups()
male_ru = translated.get(male, "") if male else ""
female_ru = translated.get(female, "") if female else ""
if male_ru or female_ru:
rows[int(entry)] = (male_ru, female_ru, int(build))
source = ROOT / "sql/sylvania/mardum_fr_broadcast.sql"
for line in source.read_text(encoding="utf-8").splitlines():
match = TMP_BROADCAST_ROW.match(line)
if not match or match.group(2) not in translated:
continue
entry = int(match.group(1))
dynamic_rows[entry] = translated[match.group(2)]
output = OUT_DIR / "ruRU_from_french_hotfixes.sql"
with output.open("w", encoding="utf-8", newline="\n") as sql:
sql.write("-- Generated from ChatGPT WoW localisation source.csv.\n")
sql.write("-- Apply to dc_hotfixes. Existing frFR rows are not modified.\n\n")
sql.write("INSERT INTO `broadcast_text_locale` "
"(`ID`,`locale`,`Text_lang`,`Text1_lang`,`VerifiedBuild`) VALUES\n")
values = []
for entry, (male, female, build) in sorted(rows.items()):
values.append(
f"({entry},'ruRU',{sql_quote(male)},{sql_quote(female)},{build})"
)
sql.write(",\n".join(values))
sql.write("\nON DUPLICATE KEY UPDATE\n")
sql.write(" `Text_lang`=IF(VALUES(`Text_lang`)<>'',VALUES(`Text_lang`),`Text_lang`),\n")
sql.write(" `Text1_lang`=IF(VALUES(`Text1_lang`)<>'',VALUES(`Text1_lang`),`Text1_lang`),\n")
sql.write(" `VerifiedBuild`=VALUES(`VerifiedBuild`);\n")
sql.write("\nDROP TEMPORARY TABLE IF EXISTS `tmp_ruru_broadcast`;\n")
sql.write("CREATE TEMPORARY TABLE `tmp_ruru_broadcast` "
"(`id` INT PRIMARY KEY, `text_ru` TEXT) CHARACTER SET utf8mb4;\n")
sql.write("INSERT INTO `tmp_ruru_broadcast` (`id`,`text_ru`) VALUES\n")
sql.write(",\n".join(
f"({entry},{sql_quote(text)})" for entry, text in sorted(dynamic_rows.items())
))
sql.write(";\n\nINSERT INTO `broadcast_text_locale` "
"(`ID`,`locale`,`Text_lang`,`Text1_lang`,`VerifiedBuild`)\n")
sql.write("SELECT r.`id`,'ruRU',\n"
" CASE WHEN b.`Text` IS NOT NULL AND b.`Text`<>'' THEN r.`text_ru` ELSE '' END,\n"
" CASE WHEN b.`Text1` IS NOT NULL AND b.`Text1`<>'' THEN r.`text_ru` ELSE '' END,0\n"
"FROM `tmp_ruru_broadcast` r JOIN `broadcast_text` b ON b.`ID`=r.`id`\n"
"ON DUPLICATE KEY UPDATE\n"
" `Text_lang`=IF(VALUES(`Text_lang`)<>'',VALUES(`Text_lang`),`Text_lang`),\n"
" `Text1_lang`=IF(VALUES(`Text1_lang`)<>'',VALUES(`Text1_lang`),`Text1_lang`),\n"
" `VerifiedBuild`=VALUES(`VerifiedBuild`);\n"
"DROP TEMPORARY TABLE `tmp_ruru_broadcast`;\n")
return len(rows) + len(dynamic_rows)
def build_world(translated: dict[str, str]) -> int:
rows: dict[tuple[int, int, int], str] = {}
source = ROOT / "sql/sylvania/mardum_fr_creature_text.sql"
for line in source.read_text(encoding="utf-8").splitlines():
match = CREATURE_ROW.match(line)
if not match or match.group(4) not in translated:
continue
creature, group, entry, french = match.groups()
rows[(int(creature), int(group), int(entry))] = translated[french]
output = OUT_DIR / "ruRU_from_french_world.sql"
with output.open("w", encoding="utf-8", newline="\n") as sql:
sql.write("-- Generated from ChatGPT WoW localisation source.csv.\n")
sql.write("-- Apply to dc_world. Existing frFR rows are not modified.\n\n")
sql.write("INSERT INTO `creature_text_locale` "
"(`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`) VALUES\n")
values = []
for (creature, group, entry), text in sorted(rows.items()):
values.append(
f"({creature},{group},{entry},'ruRU',{sql_quote(text)})"
)
sql.write(",\n".join(values))
sql.write("\nON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);\n\n")
# These two source rows are assembled dynamically in the French module.
replacements = [
(97058, 1, 0, [("begins to cast", "начинает произносить"),
("[Carrion Storm]", translated.get("[Tempête de charogne]", "[Буря падали]"))]),
(97059, 2, 0, [("Nearby", "Ближайшие"),
("[Spider Eggs]", translated.get("[Œufs daraignée]", "[Паучьи яйца]")),
("will hatch soon if not destroyed!.", "скоро вылупятся, если их не уничтожить!")]),
]
for creature, group, entry, pairs in replacements:
expression = "ct.`Text`"
for old, new in pairs:
expression = f"REPLACE({expression}, {sql_quote(old)}, {sql_quote(new)})"
sql.write(
"INSERT INTO `creature_text_locale` (`CreatureID`,`GroupID`,`ID`,`Locale`,`Text`)\n"
f"SELECT ct.`CreatureID`,ct.`GroupID`,ct.`ID`,'ruRU',{expression}\n"
f"FROM `creature_text` ct WHERE ct.`CreatureID`={creature} "
f"AND ct.`GroupID`={group} AND ct.`ID`={entry}\n"
"ON DUPLICATE KEY UPDATE `Text`=VALUES(`Text`);\n\n"
)
gossip_fr = (
"Whitemoon, je vais vous emprunter une de vos chauves-souris gangrenées. "
"Nous allons chercher ce pour quoi nous sommes venus."
)
if gossip_fr in translated:
sql.write(
"INSERT INTO `gossip_menu_option_locale` "
"(`MenuId`,`OptionIndex`,`Locale`,`OptionText`,`BoxText`) VALUES\n"
f"(18776,0,'ruRU',{sql_quote(translated[gossip_fr])},'')\n"
"ON DUPLICATE KEY UPDATE `OptionText`=VALUES(`OptionText`);\n"
)
return len(rows) + 3
def main() -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
translated = translations()
hotfixes = build_hotfixes(translated)
world = build_world(translated)
print(f"hotfixes rows: {hotfixes}")
print(f"world rows: {world}")
if __name__ == "__main__":
main()