Phase 21's infra half. Two things the pt-PT pair needs from TTS, and one thing every learner has wanted since Phase 11. **A language is no longer a code change.** The handler knew exactly two languages, named in the Config struct: English on TTS_ENDPOINT and Chinese on TTS_ENDPOINT_ZH. Petal now discovers its Piper instances from the environment — English keeps the unsuffixed pair it has always had, and every other language is a TTS_ENDPOINT_<LANG>/TTS_VOICE_<LANG> pair — so fr and es cost a compose service and two lines of .env. <LANG> is the base tag, because an environment variable name cannot hold pt-PT's hyphen and only one Portuguese model is loaded either way. A language configured by halves is dropped rather than routed: half a configuration should reach the client as "no voice here, use Web Speech", not as an instance that errors on every tap. The startup line now names the voices it actually resolved rather than the English endpoint it was handed — the same lesson the dictionary line learned last week. **pt_PT-tugão-medium is the only European voice Piper ships.** The other five pt models in the catalogue are Brazilian, so the default anyone reaches for is the wrong country — the same trap as `dictionary-pt` packaging VERO, arriving through the catalogue rather than through the model. Named explicitly in compose, with the query that checks it in the deploy README. **The slow replay** (SUGGESTIONS §5e) is `slow: true` on /api/tts, raising Piper's length_scale to ~4/3. Piper stretches durations rather than resampling, so it stays a voice instead of a groan. The pace is part of the cache key — without it the slow replay of a word already heard at normal speed would be served back at normal speed, which is the one request where the difference is the whole point. 🐢 sits beside 🔊 on the word card, the selection bubble and the garden flashcard; the Web Speech fallback slows too, so the button means the same thing when Piper is down. **And the other reading gets her own voice.** The `alsoIn` block — the Portuguese sense of a word that is also English — now speaks in the pair's locale, which the pack names (`locale`) rather than anything inferring it from the letters. "comum" is spelled identically in both halves; a detector would have to guess, and this is the same reason the gloss shows both directions instead of picking one. Tests: config discovery (both existing deployment shapes, half-configured languages dropped, the pre-map voice defaults preserved), the slow scale and its separate cache entry, pt routing on the base tag with pt-BR landing on the European instance, and speech.ts's request body. The i18n shape suite now asserts every pack names a speakable locale in its own language — and that pt-PT's is not pt-BR. Verified: go build/vet/test, tsc, vitest 125/125, vite build. Live smoke against two fake Piper servers: en/pt × normal/slow all reached the right instance at the right length_scale with four distinct cache entries, and an unconfigured language still 404s.
305 lines
15 KiB
TypeScript
305 lines
15 KiB
TypeScript
// The Mandarin pack — Petal's original copy, moved here unchanged.
|
||
//
|
||
// Every string in this file was a literal somewhere in the app before Phase 19.
|
||
// It is deliberately verbatim, down to the punctuation: the zh pair is in daily
|
||
// use, and a langpack extraction that quietly reworded anything would be a
|
||
// product change wearing a refactor's clothes.
|
||
//
|
||
// Mandarin first, English underneath — she reads Chinese faster, and the
|
||
// English subtitle is what she's here to learn.
|
||
|
||
import type { Pack } from '../types'
|
||
|
||
export const zh: Pack = {
|
||
code: 'zh',
|
||
nativeName: '中文',
|
||
locale: 'zh-CN',
|
||
|
||
app: {
|
||
duplicateTitle: (title) => `${title} (副本)`,
|
||
garden: '词汇花园',
|
||
history: '历史',
|
||
},
|
||
|
||
auth: {
|
||
title: '请重新登录',
|
||
titleEn: 'Please sign in again',
|
||
bodyWithDraft: '你刚写的内容已经安全地留在这台电脑上,登录后会自动接着保存。',
|
||
bodyWithDraftEn: "What you just wrote is safe on this device — it'll save itself once you're back in.",
|
||
bodyPlain: '登录状态过期了。你的文字都已经保存好了。',
|
||
bodyPlainEn: 'Your session expired. Everything you wrote is already saved.',
|
||
signIn: '去登录 · Sign in',
|
||
},
|
||
|
||
companion: {
|
||
choose: '选个小伙伴 · Choose a companion',
|
||
|
||
// Played when a suggestion is accepted or a milestone hits — pure warmth.
|
||
encouragements: [
|
||
{ native: '好棒!这一句更顺了 🌸', en: 'Lovely — that reads so much smoother now.' },
|
||
{ native: '你写得越来越好了 ✨', en: "You're getting better and better." },
|
||
{ native: '我很喜欢这个改法 💕', en: 'I really like that change.' },
|
||
{ native: '继续保持,加油!', en: 'Keep going — you’ve got this!' },
|
||
{ native: '嗯嗯,这样清楚多了 👍', en: 'Mm, that’s much clearer.' },
|
||
{ native: '这个词用得真好 🌷', en: 'That’s such a good word choice.' },
|
||
{ native: '哇,这一段读起来真舒服 ☁️', en: 'Ooh, that paragraph flows so nicely.' },
|
||
{ native: '看你越写越有信心,真好 💛', en: 'I love watching you write with more confidence.' },
|
||
{ native: '一点点进步,都是了不起的进步 🌱', en: 'Every little bit of progress counts.' },
|
||
{ native: '今天的你,文字闪闪发光 ✨', en: 'Your words are sparkling today.' },
|
||
],
|
||
|
||
// Gentle, generic writing/ESL tips — the fallback when the rules-based prose
|
||
// checker finds nothing concrete to point at in her current text.
|
||
tips: [
|
||
{ native: '小贴士:英文句子短一点,会更清楚哦。', en: 'Tip: shorter English sentences often read clearer.' },
|
||
{ native: '别忘了冠词 “the” 和 “a” 哦。', en: "Don't forget articles like “the” and “a”." },
|
||
{ native: '过去的事情用过去式:go → went。', en: 'For the past, use past tense: go → went.' },
|
||
{ native: '读出声音,能帮你发现奇怪的地方。', en: 'Reading aloud helps you catch awkward spots.' },
|
||
{ native: '一个段落讲一个想法就好。', en: 'One idea per paragraph keeps it tidy.' },
|
||
{ native: '不确定的地方,问问我就好啦 ✨', en: 'Not sure about something? Just ask me. ✨' },
|
||
{ native: '复数别忘了加 s:two apples 🍎', en: 'Plurals take an “s”: two apples 🍎' },
|
||
],
|
||
|
||
// Shown after a long stretch of continuous writing.
|
||
breaks: [
|
||
{ native: '写了好一会儿啦,起来走走,让眼睛休息一下 🍵', en: "You've been writing a while — stretch and rest your eyes. 🍵" },
|
||
{ native: '喝口水,休息五分钟好不好?', en: 'Sip some water and take five?' },
|
||
{ native: '看看远方,放松一下眼睛 🌿', en: 'Look into the distance for a moment — give your eyes a break. 🌿' },
|
||
],
|
||
|
||
// Shown when she's still writing late at night (≥11pm). Caring, a little
|
||
// playful — the kitten is always asleep, so "you should be too" lands as a
|
||
// gag, never a scold. The native line stays gentle; English carries the wink.
|
||
bedtime: [
|
||
{ native: '你的床在想你了哦 🛏️', en: 'I bet your bed is missing you right now.' },
|
||
{ native: '太累可写不出好文字呀,早点歇着吧 🌙', en: 'A tired writer is a bad writer — get some rest.' },
|
||
{ native: '好好睡一觉,灵感自己会来 ✨', en: 'Sleep is a wondrous enabler.' },
|
||
{ native: '听见了吗?没有吧——大家都睡了,你也该睡啦 😴', en: "Hear that? No… you don't, because everyone is sleeping and you should be too." },
|
||
// A few old Chinese proverbs on sleep (the native line is a faithful
|
||
// rendering of the English sense — the verified classical 原文 isn't
|
||
// recoverable; swap in the exact source text if you have it). They read a
|
||
// touch wittier/wiser than the gentle lines above, which suits a
|
||
// late-night nudge.
|
||
{ native: '一夜不眠,十日不安。', en: "The loss of one night's sleep is followed by ten days of inconvenience." },
|
||
{ native: '前半夜醒着想自己的过错,后半夜睡着才想别人的不是。', en: 'Think of your own faults the first part of the night when you are awake, and of the faults of others the latter part of the night when you are asleep.' },
|
||
{ native: '黄昏与人相骂,夜半独自难眠。', en: 'Curse your spouse at evening, sleep alone at night.' },
|
||
],
|
||
|
||
greeting: { native: '嗨~我在这儿陪你写作哦 🐱', en: "Hi! I'm right here keeping you company. 🐱" },
|
||
welcomeBack: { native: '欢迎回来 ✨ 我们继续吧!', en: 'Welcome back ✨ let’s keep going!' },
|
||
|
||
errors: [
|
||
{ native: '哎呀~出了点小问题,你的字都还在哦。', en: 'Oops — a little hiccup, but your words are safe.' },
|
||
{ native: '哎呀,我这边卡了一下,马上就好。', en: 'Haiya, I got stuck for a sec — back in a moment.' },
|
||
{ native: '别担心,等一下再试试看 🍵', en: "Don't worry — let's try again in a bit. 🍵" },
|
||
],
|
||
|
||
milestone: (words: number) => ({
|
||
native: `哇!已经 ${words} 个词了,太厉害了 🎉`,
|
||
en: `Wow — ${words} words already! Amazing. 🎉`,
|
||
}),
|
||
|
||
// Keyed by the companion ids in Companion/companions.ts.
|
||
names: {
|
||
cat: '瞌睡猫',
|
||
dog: '开心狗',
|
||
'wiggle-dog': '摇尾狗',
|
||
butterfly: '蝴蝶',
|
||
parrot: '鹦鹉',
|
||
},
|
||
},
|
||
|
||
prose: {
|
||
longSentence: '这句话有点长啦,分成两三句会更清楚 🌸',
|
||
commaSplice: '这里用逗号连了两句话,可以改成句号,或加个 “and / but”。',
|
||
vagueThis: (word) => `“${word}” 指代不太清楚,最好点明它指的是什么(比如 “${word} idea / change…”)。`,
|
||
oxfordComma: '列举三样以上时,在 “and / or” 前也加个逗号会更清楚(牛津逗号)。',
|
||
transitionComma: (word) => `开头的过渡词后面加个逗号:“${word}, …”。`,
|
||
capitalizeSentence: '每个句子的开头,用大写字母开始吧。',
|
||
repeatedWord: (word) => `“${word}” 好像写了两遍,检查一下哦。`,
|
||
capitalizeI: '英文里的 “I”(我)任何时候都要大写哦。',
|
||
spaceBeforePunct: '标点前面不用空格,逗号、句号紧跟在前一个词后面就好。',
|
||
spaceAfterPunct: '逗号、句号后面要空一格,再接下一个词。',
|
||
articleAn: (word) => `元音开头的词前用 “an”:“an ${word}”。`,
|
||
articleA: (word) => `辅音开头的词前用 “a”:“a ${word}”。`,
|
||
uncountable: (word, singular) => `“${word}” 是不可数名词,不用加 s,写 “${singular}” 就好。`,
|
||
capitalizeProper: (fixed) => `语言、国籍、星期和月份在英文里要大写:“${fixed}”。`,
|
||
thirdPersonS: (subject, verb) => `主语是 he/she/it 时,动词要加 -s:“${subject} ${verb}”。`,
|
||
pluralAfter: (determiner, noun) => `“${determiner}” 后面的名词要用复数:“${determiner} ${noun}s”。`,
|
||
doubleDeterminer: (first, second) => `“${first} ${second}” 用了两个限定词,留一个就好(比如去掉 “${first}”)。`,
|
||
thereArePlural: (noun) => `后面是复数时用 “there are”:“there are ${noun}…”。`,
|
||
itsOwn: '“it’s” = “it is”;表示“它的”要用 “its”,所以是 “its own”。',
|
||
itsIs: (rest) => `这里应该是 “it’s ${rest}”(it is),“its” 是“它的”。`,
|
||
thanNotThen: (word) => `比较的时候用 “than”,不是 “then”:“${word} than”。`,
|
||
},
|
||
|
||
docs: {
|
||
sortRecent: '最近 · Recent',
|
||
sortTitle: '标题 · Title',
|
||
sortLongest: '字数 · Longest',
|
||
backUpAll: '备份 · Back up all:',
|
||
signOut: '退出 · Sign out',
|
||
duplicate: '副本 · Duplicate',
|
||
searchPlaceholder: '搜索 · Search',
|
||
searching: '查找中… · Searching…',
|
||
noMatches: '没有找到 · No matches',
|
||
tags: '标签 · Tags',
|
||
newTagPlaceholder: '新标签 · New tag',
|
||
},
|
||
|
||
editor: {
|
||
askPlaceholder: 'Ask why… / 问为什么…',
|
||
findPlaceholder: '查找 · Find',
|
||
findNone: '无 · 0',
|
||
matchCase: 'Match case · 区分大小写',
|
||
close: 'Close · 关闭',
|
||
replacePlaceholder: '替换为 · Replace',
|
||
replace: '替换',
|
||
replaceAll: '全部',
|
||
spelling: '拼写 · Spelling',
|
||
noSuggestions: '没有建议 · No suggestions',
|
||
addToDictionary: '添加到词典 · Add to dictionary',
|
||
readSelection: '朗读所选 · Read selection aloud',
|
||
rewrite: '改写 · Rewrite',
|
||
rewriting: '改写中… · Rewriting…',
|
||
rewriteFailed: '改写失败,请再试一次 · Couldn’t rewrite — try again',
|
||
cancel: '取消 · Cancel',
|
||
retry: '重试 · Retry',
|
||
useThis: '用这个 · Use this',
|
||
word: '词语 · Word',
|
||
inGarden: '已在词汇花园 · In your garden (tap to remove)',
|
||
saveToGarden: '加入词汇花园 · Save to garden',
|
||
readAloud: '朗读 · Read aloud',
|
||
readSlowly: '慢速朗读 · Read slowly',
|
||
readAloudNative: '用中文朗读 · Read in Chinese',
|
||
lookingUp: '查找中… · Looking up…',
|
||
definition: '释义 · Definition',
|
||
synonyms: '近义词 · Synonyms',
|
||
tapToSwap: '点击替换 · tap to swap',
|
||
nothingFound: '没有找到这个词 · Nothing found for this word',
|
||
origin: '词源 · Origin',
|
||
// Never rendered for this pair — English and Chinese share no spellings, so
|
||
// a word is never both. Written out anyway because the type demands it and
|
||
// because "unreachable" is a claim about today's data, not a guarantee.
|
||
alsoIn: '这个词在中文里也有 · Also a word in Chinese',
|
||
wordBands: {
|
||
simple: { native: '常用词', en: 'Everyday word' },
|
||
standard: { native: '一般难度', en: 'Standard' },
|
||
advanced: { native: '进阶词', en: 'Advanced' },
|
||
},
|
||
},
|
||
|
||
styles: {
|
||
natural: { native: '更自然', en: 'Natural' },
|
||
academic: { native: '学术', en: 'Academic' },
|
||
professional: { native: '专业', en: 'Professional' },
|
||
casual: { native: '轻松', en: 'Casual' },
|
||
humorous: { native: '幽默', en: 'Humorous' },
|
||
creative: { native: '创意', en: 'Creative' },
|
||
persuasive: { native: '说服', en: 'Persuasive' },
|
||
},
|
||
|
||
tones: {
|
||
general: { native: '通用', en: 'General' },
|
||
academic: { native: '学术', en: 'Academic' },
|
||
professional: { native: '专业', en: 'Professional' },
|
||
casual: { native: '轻松', en: 'Casual' },
|
||
humorous: { native: '幽默', en: 'Humorous' },
|
||
creative: { native: '创意', en: 'Creative' },
|
||
persuasive: { native: '说服', en: 'Persuasive' },
|
||
},
|
||
|
||
exports: {
|
||
label: '导出',
|
||
print: '打印 / PDF',
|
||
formats: {
|
||
md: { native: 'Markdown', en: 'Markdown (.md)' },
|
||
docx: { native: 'Word 文档', en: 'Word (.docx)' },
|
||
html: { native: '网页', en: 'Web page (.html)' },
|
||
txt: { native: '纯文本', en: 'Plain text (.txt)' },
|
||
},
|
||
},
|
||
|
||
garden: {
|
||
title: '词汇花园 · Vocabulary Garden',
|
||
titleWithFlower: '🌷 词汇花园 · Vocabulary Garden',
|
||
reviewing: '复习中 · Reviewing — recall, then grade yourself',
|
||
subtitle: 'Words you looked up, blooming as you learn them',
|
||
reviewDue: (n) => `复习 ${n} 个词 · Review ${n} due 🌸`,
|
||
emptyLead: '你的花园还空着。',
|
||
emptyHint: '右键点一个英文单词查它的意思——它就会在这里发芽。',
|
||
due: '待复习 · due',
|
||
seen: (reps, intervalDays) => `复习 ${reps} 次 · seen ${reps}× · 间隔 ${intervalDays}d`,
|
||
readAloud: '🔊 朗读',
|
||
readSlowly: '🐢 慢速',
|
||
source: '📄 出处 · Source',
|
||
remove: '🗑 移除',
|
||
growing: (n) => `🐱💤 ${n} 朵花在花园里 · ${n} blossom${n > 1 ? 's' : ''} growing`,
|
||
end: '结束 · End',
|
||
promptProduction: '这个中文意思的英文单词是?· Which English word?',
|
||
promptRecognition: '这个词什么意思?· What does this mean?',
|
||
showAnswer: '翻看答案 · Show answer',
|
||
gradeAgain: { native: '再来', en: 'Again' },
|
||
gradeGood: { native: '记得', en: 'Good' },
|
||
gradeEasy: { native: '太简单', en: 'Easy' },
|
||
},
|
||
|
||
history: {
|
||
title: '历史 · History',
|
||
kinds: {
|
||
manual: { native: '保存点', en: 'Saved point' },
|
||
auto: { native: '自动', en: 'Auto' },
|
||
pre_restore: { native: '恢复前', en: 'Before restore' },
|
||
},
|
||
justNow: 'just now · 刚刚',
|
||
minutesAgo: (n) => `${n} min ago · ${n} 分钟前`,
|
||
hoursAgo: (n) => `${n} hr ago · ${n} 小时前`,
|
||
daysAgo: (n) => `${n} day${n > 1 ? 's' : ''} ago · ${n} 天前`,
|
||
preview: '预览 · Preview',
|
||
restoring: 'Restoring…',
|
||
restoreThis: '恢复这个版本 · Restore this version',
|
||
passport: '📜 写作证明 · Writing passport',
|
||
keepFullHistory: '保留完整历史 · Keep full history',
|
||
},
|
||
|
||
status: {
|
||
savedLocally: '已保存在本机 · Kept on this device',
|
||
helperRestingNative: '小助手在休息',
|
||
helperRestingEn: "· Petal's helper is resting · 文字已保存",
|
||
soundsOn: '声音开 · Sounds on',
|
||
soundsOff: '声音关 · Sounds off',
|
||
petalsOn: '花瓣开 · Petals on',
|
||
petalsOff: '花瓣关 · Petals off',
|
||
statsTitle: '写作统计 · Writing stats',
|
||
stats: {
|
||
words: { native: '字数', en: 'Words' },
|
||
characters: { native: '字符', en: 'Characters' },
|
||
sentences: { native: '句子', en: 'Sentences' },
|
||
paragraphs: { native: '段落', en: 'Paragraphs' },
|
||
pages: { native: '页数', en: 'Pages' },
|
||
readingTime: { native: '阅读时间', en: 'Reading time' },
|
||
avgWordLength: { native: '平均词长', en: 'Avg word length' },
|
||
variety: { native: '词汇丰富度', en: 'Word variety' },
|
||
readability: { native: '阅读难度', en: 'Reading level' },
|
||
},
|
||
readability: {
|
||
easy: { native: '简单', en: 'Easy' },
|
||
standard: { native: '标准', en: 'Standard' },
|
||
fairlyHard: { native: '偏难', en: 'Fairly hard' },
|
||
advanced: { native: '较难', en: 'Advanced' },
|
||
},
|
||
},
|
||
|
||
toolbar: {
|
||
untitledHeading: '(无标题)',
|
||
outline: '大纲 · Outline',
|
||
outlineHint: '用 H1/H2/H3 添加标题,这里就会出现导航。',
|
||
},
|
||
|
||
update: {
|
||
available: '有新版本啦',
|
||
refresh: '刷新 · Refresh',
|
||
dismiss: '稍后再说 · Dismiss',
|
||
},
|
||
}
|