Files
UmbraAudio/scripts/build_sample_translations.py
B.D. Murphy bf83bb35cc Merge minieq/main: Apache-2.0 LICENSE, NOTICE and SPDX headers
The remote had one commit this branch did not (03f8a85), which adds the
Apache-2.0 LICENSE, the NOTICE and SPDX headers to every .py and .sh
file. The merge was clean - the headers sit at the top of each file and
my changes are further down.

The new files this branch added (languages.py, translations.py, the
cluster package, the new tests and scripts) were written after the
merge base, so the licence commit never saw them. They now carry the
same header, so the convention holds across the whole tree.

Nothing else changed. 883 tests passed / 8 failed - the same 8
pre-existing failures.
2026-11-06 17:12:24 -08:00

365 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Generate the Deal Hunter sample chapter in every language Qwen3-TTS produces.
IMPORTANT - what this is and is not
-----------------------------------
These translations were produced by an LLM in a single pass, by hand, as a
SAMPLE. They are not the output of the multi-pass semantic translation pipeline
described in specs/27, which does not exist yet. They exist so that the audio
stage can be evaluated per language and so the file format can be agreed
before the pipeline is built.
They need native-speaker review before any of them is used for anything real.
Decisions taken, and flagged because the pipeline must decide them properly:
* "Deal Hunter" and "Umbra Audio" are kept in English everywhere. Deal Hunter
is both the book title and the name of the ship AI, so translating it would
break the referent inside the story. This is a localisation decision, not a
translation one, and it is the operator's to confirm.
* Character names are transliterated by sound (Kainda -> 凯因达 / カインダ),
not translated by meaning. A name is an identity, not a word.
``validate()`` runs before anything is written. An earlier draft of this file
shipped stray English fragments and a duplicated Japanese entry; the checks
below exist so that class of corruption cannot reach a file silently.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
# The tracked record of record. It used to default to scripts/translations,
# which nothing reads and nothing tracks, so regenerating left the tracked copy
# and the renderer's own copy both silently stale.
OUT = REPO_ROOT / "samples" / "translations"
SAMPLE_PROJECT = "DealHunterShip_4.0e_whole-to-audio"
# The 10 languages Qwen3-TTS produces.
LANGUAGES = {
"EN": ("English", "en"),
"ZH": ("Chinese", "zh"),
"JA": ("Japanese", "ja"),
"KO": ("Korean", "ko"),
"DE": ("German", "de"),
"FR": ("French", "fr"),
"RU": ("Russian", "ru"),
"PT": ("Portuguese", "pt"),
"ES": ("Spanish", "es"),
"IT": ("Italian", "it"),
}
# (speaker, type) per utterance, matching the source chapter.
# (speaker, type). Type must follow the speaker: the Narrator reads the
# introduction, everyone else speaks a line of dialogue. These were all
# "narration", which made every row render a narration badge and would tell
# UmbraTrans that six characters are not speaking at all.
SHAPE = [
("Narrator", "narration"),
("Kainda", "dialogue"),
("Narrator", "narration"),
("Duchess Victoria", "dialogue"),
("Narrator", "narration"),
("Deal Hunter", "dialogue"),
("Nanny", "dialogue"),
("Narrator", "narration"),
("Trader Romano", "dialogue"),
("Narrator", "narration"),
("Narrator", "narration"),
("Deal Hunter", "dialogue"),
]
TRANSLATIONS: dict[str, list[str]] = {
"EN": [
'Welcome to the audiobook Deal Hunter. I am your narrator, and I will be introducing several of the voices you are about to hear. Princess Kainda, please say hello.',
'Hello! I am Princess Kainda, the main character of the story. My adventure will begin shortly.',
'Thank you, Kainda. Duchess Victoria, please say hello.',
"Hello! I am Duchess Victoria. I am also Kainda's aunt. I cannot wait to share this adventure with you.",
'There are other voices you will hear as well. Deal Hunter, or Hunter, please say hello.',
'Hello! I am the AI that runs the ship. I am usually just called Hunter. I am looking forward to entertaining you with this story. One of my robots, whom Kainda decided to name Nanny, is here. Say hello, Nanny.',
'Hello! Kainda named me Nanny because it is easier to use than my robot designation. I will be as helpful as I can.',
'I will introduce one more voice. Please say hello.',
'Hello, everyone! I am Romano. I trade salvage with Deal Hunter, always giving the best price. Come by the station if you want to trade.',
'These voices, and more, were created by B. D. Murphy in his Umbra Audio system to voice his original book, Deal Hunter. The project is to make AI-assisted audiobook production possible.',
'We are all looking forward to entertaining you with the audio version of Deal Hunter.',
'Thank you for listening!',
],
"FR": [
'Bienvenue dans le livre audio Deal Hunter. Je suis ton narrateur et je vais te présenter plusieurs des voix que tu vas entendre. Princesse Kainda, dis bonjour !',
"Bonjour ! Je suis la princesse Kainda, l'héroïne principale de cette histoire. Mon aventure commence dans un instant.",
'Merci, Kainda. Duchesse Victoria, dis bonjour !',
"Bonjour ! Je suis la duchesse Victoria. Je suis aussi la tante de Kainda. J'ai hâte de partager cette aventure avec toi.",
"Tu entendras aussi d'autres voix. Deal Hunter, ou Hunter, dis bonjour !",
"Bonjour ! Je suis l'intelligence artificielle qui dirige le vaisseau. On m'appelle d'ordinaire simplement Hunter. J'ai hâte de te faire découvrir cette histoire. Voici l'un de mes robots, que Kainda a décidé de surnommer Nanny. Dis bonjour, Nanny.",
"Bonjour ! Kainda m'a appelée Nanny parce que c'est plus facile à prononcer que ma désignation de robot. Je ferai de mon mieux pour t'être utile.",
'Je vais te présenter une dernière voix. Dis bonjour !',
"Bonjour à tous ! Je suis Romano. Je fais commerce de ferraille avec le Deal Hunter, et j'offre toujours le meilleur prix. Viens à la station si tu souhaites échanger.",
"Ces voix, et bien d'autres, ont été créées par B. D. Murphy au sein de son système Umbra Audio pour donner voix à son livre original, Deal Hunter. Le projet est de rendre possible la production de livres audio assistée par IA.",
"Nous avons tous hâte de t'offrir la version audio de Deal Hunter.",
"Merci d'avoir écouté !",
],
"ES": [
'Bienvenidos al audiolibro Deal Hunter. Soy tu narrador y voy a presentarte varias de las voces que vas a oír. Princesa Kainda, por favor, saluda.',
'¡Hola! Soy la princesa Kainda, la protagonista de la historia. Mi aventura comenzará en breve.',
'Gracias, Kainda. Duquesa Victoria, por favor, saluda.',
'¡Hola! Soy la duquesa Victoria. También soy la tía de Kainda. No veo la hora de compartir esta aventura contigo.',
'También oirás otras voces. Deal Hunter, o Hunter, por favor, saluda.',
'¡Hola! Soy la inteligencia artificial que controla la nave. Normalmente me llaman solo Hunter. Tengo muchas ganas de entretenerte con esta historia. Aquí está uno de mis robots, al que Kainda decidió llamar Nanny. Di hola, Nanny.',
'¡Hola! Kainda me llamó Nanny porque es más fácil de usar que mi designación de robot. Seré tan servicial como pueda.',
'Voy a presentar una voz más. Por favor, saluda.',
'¡Hola a todos! Soy Romano. Compro chatarra al Deal Hunter y siempre ofrezco el mejor precio. Ven a la estación si quieres comerciar.',
'Estas voces, y muchas más, fueron creadas por B. D. Murphy en su sistema Umbra Audio para dar voz a su libro original, Deal Hunter. El proyecto es hacer posible la producción de audiolibros asistida por IA.',
'Todos estamos deseando entretenerte con la versión de audio de Deal Hunter.',
'¡Gracias por escuchar!',
],
"PT": [
'Bem-vindos ao audiolivro Deal Hunter. Sou o teu narrador e vou apresentar-te algumas das vozes que vais ouvir. Princesa Kainda, por favor, dá um olhar.',
'Olá! Sou a princesa Kainda, a personagem principal da história. A minha aventura começa em breve.',
'Obrigada, Kainda. Duquesa Victoria, por favor, dá um olhar.',
'Olá! Sou a duquesa Victoria. Sou também tia de Kainda. Mal posso esperar para partilhar esta aventura contigo.',
'Vais também ouvir outras vozes. Deal Hunter, ou Hunter, por favor, dá um olhar.',
'Olá! Sou a inteligência artificial que controla a nave. Normalmente chamam-me apenas de Hunter. Estou ansioso para te entreter com esta história. Aqui está um dos meus robôs, que a Kainda decidiu chamar de Nanny. Diz olá, Nanny.',
'Olá! A Kainda chamou-me de Nanny porque é mais fácil de usar do que a minha designação de robô. Vou ser tão prestável quanto puder.',
'Vou apresentar mais uma voz. Por favor, dá um olhar.',
'Olá a todos! Sou o Romano. Compro sucata ao Deal Hunter e sempre ofereço o melhor preço. Vem à estação se quiseres negociar.',
'Estas vozes, e muitas outras, foram criadas por B. D. Murphy no seu sistema Umbra Audio para dar voz ao seu livro original, Deal Hunter. O projeto é tornar possível a produção de audiolivros assistida por IA.',
'Estamos todos ansiosos para te entreter com a versão em áudio de Deal Hunter.',
'Obrigado por ouvires!',
],
"IT": [
"Benvenuti all'audiolibro Deal Hunter. Sono il tuo narratore e ti presenterò alcune delle voci che potrai sentire. Principessa Kainda, per favore, saluta.",
'Ciao! Sono la principessa Kainda, la protagonista della storia. La mia avventura inizierà tra poco.',
'Grazie, Kainda. Duchessa Victoria, per favore, saluta.',
"Ciao! Sono la duchessa Victoria. Sono anche la zia di Kainda. Non vedo l'ora di condividere questa avventura con te.",
'Sentirai anche altre voci. Deal Hunter, o Hunter, per favore, saluta.',
"Ciao! Sono l'intelligenza artificiale che governa la nave. Di solito mi chiamano solo Hunter. Non vedo l'ora di intrattenerti con questa storia. Ecco uno dei miei robot, che Kainda ha deciso di chiamare Nanny. Saluta, Nanny.",
'Ciao! Kainda mi ha chiamata Nanny perché è più semplice da usare della mia designazione di robot. Farò il mio meglio per essere utile.',
"Ti presenterò un'altra voce. Per favore, saluta.",
'Ciao a tutti! Sono Romano. Compro rottami dal Deal Hunter e offro sempre il prezzo migliore. Vieni alla stazione se vuoi scambiare.',
"Queste voci, e molte altre, sono state create da B. D. Murphy nel suo sistema Umbra Audio per dare voce al suo libro originale, Deal Hunter. Il progetto è rendere possibile la produzione di audiolibri assistita dall'IA.",
"Non vediamo l'ora di intrattenerti con la versione audio di Deal Hunter.",
'Grazie per aver ascoltato!',
],
"DE": [
'Willkommen zum Hörbuch „Deal Hunter“. Ich bin dein Erzähler und stelle dir gleich mehrere der Stimmen vor, die du hören wirst. Prinzessin Kainda, bitte grüß.',
'Hallo! Ich bin Prinzessin Kainda, die Hauptfigur der Geschichte. Mein Abenteuer beginnt gleich.',
'Danke, Kainda. Herzogin Victoria, bitte grüß.',
'Hallo! Ich bin Herzogin Victoria. Ich bin außerdem Kaindas Tante. Ich kann es kaum erwarten, dieses Abenteuer mit dir zu teilen.',
'Du wirst außerdem weitere Stimmen hören. Deal Hunter, oder Hunter, bitte grüß.',
'Hallo! Ich bin die künstliche Intelligenz, die das Schiff steuert. Man nennt mich gewöhnlich einfach Hunter. Ich freue mich darauf, dir mit dieser Geschichte eine schöne Zeit zu bereiten. Hier ist einer meiner Roboter, den Kainda auf den Namen Nanny getauft hat. Sag Hallo, Nanny.',
'Hallo! Kainda hat mich Nanny genannt, weil das leichter zu sagen ist als meine Roboternummer. Ich werde so hilfreich sein, wie ich kann.',
'Ich stelle noch eine Stimme vor. Bitte grüß.',
'Hallo zusammen! Ich bin Romano. Ich kaufe Schrott vom Deal Hunter und biete stets den besten Preis. Komm zur Station, wenn du etwas tauschen möchtest.',
'Diese Stimmen und weitere wurden von B. D. Murphy in seinem Umbra-Audio-System erzeugt, um seinem Originalroman „Deal Hunter“ eine Stimme zu geben. Das Projekt soll die KI-gestützte Erstellung von Hörbüchern ermöglichen.',
'Wir alle freuen uns darauf, dir mit der Audiofassung von „Deal Hunter“ eine schöne Zeit zu bereiten.',
'Vielen Dank fürs Zuhören!',
],
"RU": [
'Добро пожаловать в аудиокнигу «Deal Hunter»! Я ваш рассказчик — сейчас прозвучат несколько голосов. Принцесса Кайнда, поздоровайте!',
'Здравствуйте! Я принцесса Кайнда, главный герой этой истории. Моё приключение начнётся совсем скоро.',
'Спасибо, Кайнда! Герцогиня Виктория, поздоровайте!',
'Здравствуйте! Я герцогиня Виктория. Я также тётя Кайнды. Мне не терпится разделить это приключение с вами!',
'Дальше прозвучат и другие голоса. Deal Hunter, или просто Хантер, поздоровайте!',
'Здравствуйте! Я искусственный интеллект, управляющий кораблём. Обычно меня называют просто Хантер. Я с нетерпением жду возможности рассказать вам эту историю. Вот один из моих роботов, которому Кайнда дала имя Нэнси. Поздоровайтесь, Нэнси!',
'Здравствуйте! Кайнда назвала меня Нэнси — это произносится куда проще, чем моё обозначение робота. Постараюсь быть максимально полезной.',
'Прозвучит ещё один голос. Поздоровайтесь!',
'Здравствуйте, все! Я Романо. Скупаю лом у Deal Hunter и всегда даю лучшую цену. Заходите на станцию, если захотите торговать.',
'Эти голоса и многие другие были созданы Б. Д. Мёрфи в его системе Umbra Audio, чтобы дать голос его оригинальной книге «Deal Hunter». Цель проекта — сделать возможным создание аудиокниг с помощью искусственного интеллекта.',
'Мы все с нетерпением ждём, чтобы порадовать вас аудиоверсией «Deal Hunter».',
'Спасибо, что слушали!',
],
"ZH": [
'欢迎收听有声书《Deal Hunter》!我是旁白,接下来给你介绍几段你即将听到的声音。凯因达公主,打个招呼吧!',
'大家好!我是凯因达公主,本故事的主人公。我的冒险很快就要开始了。',
'谢谢你,凯因达!维多利亚女爵,打个招呼吧!',
'大家好!我是维多利亚女爵,也是凯因达的姑母。我非常期待和你一起分享这段冒险!',
'你还会听到其他几位声音。Deal Hunter,也就是「Hunter」,打个招呼吧!',
'大家好!我是操控这艘飞船的人工智能。平常大家都叫我「Hunter」。我非常期待用这个故事为你带来乐趣。我的一位机器人同伴——凯因达给她起名叫「南妮」。南妮,打个招呼吧!',
'大家好!凯因达叫我「南妮」,因为这比我的机器人编号好叫。我会尽力帮上忙!',
'再介绍一位声音。打个招呼吧!',
'大家好,我是罗曼诺!我跟 Deal Hunter 做回收物交易,收购价格一向最优。欢迎到空间站来找我交易!',
'这些声音以及更多声音,都是由 B·D·墨菲在他的 Umbra Audio 系统中创作出来的,用以为他的原创小说《Deal Hunter》配音。这个项目的目标是让人工智能辅助的有声书制作成为可能。',
'我们大家都期待用《Deal Hunter》的有声版为你带来享受!',
'感谢收听!',
],
"JA": [
'オードブック『Deal Hunter』へようこそ!ナレーターの私です。これから将要聞かれるお声をいくつかご紹介ね。プリンセス・カインダ、あいさつをお願いね。',
'こんにちは!物語の主人公、プリンセス・カインダよ。私の冒険はもうすぐ始まるよ。',
'ありがとう、カインダ!ヴィクトリア公妃、あいさつをお願いね。',
'こんにちは!ヴィクトリア公妃よ。カインダのおばでもあるの。この冒険をあなたと一緒にできることを楽しみにしてるよ。',
'このほか、まだ聞いていない声もあるよ。Deal Hunter、ハンター、あいさつをお願いね。',
'こんにちは!この船を操っている人工知能よ。通常は「ハンター」と呼ばれてる。この物語でお楽しみいただくのを楽しみにしてるよ。カインダが「ナニー」と名づけた、私のロボットの一人がいます。ナニー、あいさつをお願いね。',
'こんにちは!カインダは、私のロボットの型番より呼びやすいって言って「ナニー」と名づけてくれたよ。できるだけ手助けするね。',
'もう一人の声をご紹介しようね。あいさつをお願いね。',
'こんにちは、みなさん!ロマーノよ。Deal Hunter と廃品回収の取引をしていて、いつも最高の値で買い取るよ。取引したかったらステーションに来てね。',
'これらの声や、もっと多くの声は、B. D. マーフィーが彼の Umbra Audio システムで作り続けて、オリジナルの小説『Deal Hunter』の声として使われてるよ。目標は、人工知能を活用した有声書の製作だよ。',
'私たち皆、『Deal Hunter』のオーディオ版でお会いできることを楽しみにしてるよ。',
'最後まで聞いてくれて、ありがとう!',
],
"KO": [
'오디오북 『Deal Hunter』에 오신 걸 환영해요! 내레이션은 제가 맡고 있어요. 지금부터 여러분이 듣게 될 목소리 몇 개를 소개할게요. 케이이다 공주님, 인사해 줘요!',
'안녕하세요! 저는 이야기의 주인공 케이이다예요. 제 모험은 곧 시작돼요.',
'고마워요, 케이이다! 빅토리아 공작부님, 인사해 줘요!',
'안녕하세요! 저는 빅토리아 공작이에요. 케이이다의 이모이기도 해요. 이 모험을 여러분이랑 함께하게 돼서 정말 기뻐요!',
'아직 들으시지 않은 목소리들도 있어요. 딜 헌터, 아니 헌터, 인사해 줘요!',
'안녕하세요! 저는 이 함선을 운항하는 인공지능이에요. 보통은 그냥 헌터라고 불러요. 이 이야기로 여러분 즐겁게 해 드리는 게 기다려져요. 케이이다가 나니라고 이름 붙여 준 제 로봇 중 한 대가 있어요. 나니, 인사해 줘요!',
'안녕하세요! 케이이다가 제 로봇 번호보다 쓰기 편하라고 저를 나니라고 불렀어요. 최대한 도와드릴게요!',
'목소리 하나만 더 소개할게요. 인사해 줘요!',
'여러분, 안녕하세요! 저는 로마노예요. 딜 헌터하고 폐기물 거래를 하면서 항상 제일 좋은 가격에 사드려요. 거래하고 싶으시면 항대로 오세요!',
'이 목소기들이랑 그 밖의 목소기들은 B. D. 머피가 자기 Umbra Audio 시스템에서 만들어서 자기 원작 『Deal Hunter』에 음성을 입힌 거예요. 이 프로젝트는 인공지능을 활용한 오디오북 제작을 가능하게 하는 게 목표예요.',
'우리 모두 『Deal Hunter』의 오디오 버전으로 여러분을 즐겁게 해 드리길 기다리고 있어요!',
'들어줘서 고마워요!',
],
}
# Only meaningful for targets that are not written in Latin script. Flagging
# Latin residue in French or German would flag every word.
NON_LATIN = {"ZH", "JA", "KO", "RU"}
# Latin text that is legitimately expected to survive translation.
ALLOWED_LATIN = re.compile(
r"Deal Hunter|Umbra Audio|B\. ?D\.?|Nanny|Hunter|Kainda|Victoria|Romano",
re.IGNORECASE,
)
LATIN_RUN = re.compile(r"[A-Za-z]{2,}")
SOURCE_ISSUES = [
"utt 3: 'Kianda's aunt' — 'Kianda' is a typo for 'Kainda'.",
"utt 5: 'the AI the runs the ship' — 'the' should be 'that'.",
"utt 8: 'I trade salvage' — double space.",
"The narrator's cues are telegraphic ('Princess Kainda say hello'), which a "
"translator has to resolve by guessing. The production pipeline should "
"decide whether to normalise these at the source.",
"Every utterance in this chapter is typed 'narration' even where a character "
"speaks in the first person. Same class of finding as the speaker-review "
"audit; it matters because emotion and pace are set per utterance.",
]
def validate() -> list[str]:
"""Catch corruption before it reaches a file."""
problems: list[str] = []
for suffix, texts in TRANSLATIONS.items():
if len(texts) != len(SHAPE):
problems.append(f"{suffix}: {len(texts)} texts, expected {len(SHAPE)}")
continue
for i, t in enumerate(texts):
if not t.strip():
problems.append(f"{suffix}[{i}]: empty")
# CJK is dense: a complete sentence can be 5 characters. Only the
# Latin-script languages get a length floor.
if suffix not in {"ZH", "JA", "KO"} and len(t) < 15:
problems.append(f"{suffix}[{i}]: suspiciously short: {t!r}")
# Stray English left behind by a bad edit.
if suffix in NON_LATIN:
residue = [
m.group(0)
for m in LATIN_RUN.finditer(ALLOWED_LATIN.sub("", t))
]
else:
residue = []
if residue:
problems.append(
f"{suffix}[{i}]: unexpected Latin text {residue} in {t[:60]!r}"
)
# A duplicated line means two variants ended up in one list entry.
if len(set(texts)) != len(texts):
dupes = {t for t in texts if texts.count(t) > 1}
problems.append(f"{suffix}: duplicated utterance text {dupes}")
# Every language must cover every utterance.
missing = set(LANGUAGES) - set(TRANSLATIONS)
if missing:
problems.append(f"no translation for: {sorted(missing)}")
return problems
def main() -> int:
problems = validate()
if problems:
print(" VALIDATION FAILED - nothing written:")
for p in problems:
print(f" - {p}")
return 1
# The renderer reads <project>/samples/translations, the repo tracks
# samples/translations. They are separate copies, so one command writes
# both rather than leaving the pair free to drift apart.
destinations = [OUT]
if "--project" in sys.argv:
idx = sys.argv.index("--project")
name = sys.argv[idx + 1] if idx + 1 < len(sys.argv) else SAMPLE_PROJECT
destinations.append(REPO_ROOT / name / "samples" / "translations")
for dest in destinations:
dest.mkdir(parents=True, exist_ok=True)
written = []
for suffix, (name, code) in LANGUAGES.items():
texts = TRANSLATIONS[suffix]
rows = [
{"index": i, "speaker": SHAPE[i][0], "type": SHAPE[i][1], "text": texts[i]}
for i in range(len(SHAPE))
]
doc = {
"project": SAMPLE_PROJECT,
"chapter_id": 1,
"chapter_title": "Sample / voice introduction",
"language_name": name,
"language_code": code,
"tts_engine": "Qwen3-TTS",
"is_source_language": suffix == "EN",
"provenance": (
"LLM-produced sample, single pass, NOT the multi-pass pipeline. "
"Requires native-speaker review."
),
"utterances": rows,
}
for dest in destinations:
path = dest / f"deal_hunter_sample_ch1_{suffix}.json"
path.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8")
written.append(f"deal_hunter_sample_ch1_{suffix}.json")
readme = (
"# Deal Hunter sample chapter - translated\n\n"
"Sample chapter 1 (the voice-introduction sample) in all 10 languages\n"
"Qwen3-TTS produces. Files are named `<base>_<LANG>.json`.\n\n"
"**These are LLM-produced samples from a single pass.** They are not the\n"
"multi-pass semantic translation the production pipeline is meant to do\n"
"(see specs/27-translation-pipeline-multipass.md). They exist to evaluate\n"
"the audio stage per language and to agree the file format early.\n"
"Native-speaker review required.\n\n"
"## Register\n\n"
"Casual/informal register throughout, matching the English source.\n"
"Korean uses 해요체 (everyday polite), NOT 합쇼체 (formal written) and\n"
"NOT 해체 (반말, which would be socially wrong between these characters).\n"
"T/V languages use the informal form (du/tu/tú). Russian keeps вы, because\n"
"ты is too intimate for narration. Japanese stays polite-but-warm.\n\n"
"## Files\n\n"
+ "".join(f"- `{n}`\n" for n in sorted(written))
+ "\n## Source issues found while translating\n\n"
+ "".join(f"- {s}\n" for s in SOURCE_ISSUES)
+ "\n## Decisions to confirm\n\n"
"- `Deal Hunter` and `Umbra Audio` kept in English in every language.\n"
" Deal Hunter is both the title and the ship AI's name, so translating it\n"
" would break the referent inside the story. Localisation decision, yours.\n"
"- Character names transliterated by sound, not translated by meaning.\n"
)
for dest in destinations:
(dest / "README.md").write_text(readme, encoding="utf-8")
for dest in destinations:
print(f" wrote {len(written)} files + README to {dest}")
for n in sorted(written):
print(f" {n}")
return 0
if __name__ == "__main__":
sys.exit(main())