diff --git a/README.md b/README.md
index b64547d..60bc180 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Privacy-first audio tools that run entirely in your browser. No uploads, no acco
| **Audio Joiner** | [/audio-joiner/](https://audiotools.dev/audio-joiner/) | Web Audio API, crossfade |
| **Audio Compressor** | [/audio-compressor/](https://audiotools.dev/audio-compressor/) | Web Audio API + Mediabunny MP3 |
-Chinese pages: [/zh/](https://audiotools.dev/zh/) and [/zh/converter/](https://audiotools.dev/zh/converter/).
+Chinese pages: [/zh/](https://audiotools.dev/zh/), [/zh/converter/](https://audiotools.dev/zh/converter/), and /zh/ versions of Volume Booster, Text to Speech, Audio Joiner and Audio Compressor.
## Architecture
@@ -57,6 +57,7 @@ audiotools.dev/
- `python3 scripts/seo_sync.py` rewrites the FAQPage JSON-LD on every page from the FAQ that is visible on that page, and refreshes `` in `sitemap.xml` from git. Run it after editing a FAQ or a page (needs `beautifulsoup4`).
- CI (`.github/workflows/seo-check.yml`) runs `scripts/seo_sync.py --check` and fails if the markup and the visible FAQ drift apart.
+- `python3 scripts/build_zh.py` rebuilds the Chinese tool pages (volume booster, text to speech, joiner, compressor) from the English ones with the translation tables in the script, and keeps hreflang, the 中文/EN switch and the Chinese navigation in step. It stops if an English string it translates has changed. Run `scripts/seo_sync.py` after it.
- Social preview images live in `og/` (1200×630, one per tool).
## Deployment
diff --git a/assistant.js b/assistant.js
index afad3d4..3c80942 100644
--- a/assistant.js
+++ b/assistant.js
@@ -137,6 +137,7 @@
const takePending = () => store('readwrite', (s) => { const g = s.get('pending'); s.delete('pending'); return g; });
// ---------- tool pages: "continue with this file" after a result ----------
+ const ZH_PAGES = ['converter', 'volume-booster', 'text-to-speech', 'audio-joiner', 'audio-compressor']; // tools with a Chinese page
const NEXT = [['audio-compressor', 'Compress', '压缩'], ['audio-trimmer', 'Trim', '剪辑'], ['volume-booster', 'Boost volume', '增强音量'],
['converter', 'Convert format', '转换格式'], ['audio-speed-changer', 'Change speed', '变速'], ['audio-joiner', 'Join with more', '合并']];
window.offerNext = function (tool, anchor, file) {
@@ -148,7 +149,7 @@
el.append(T('Continue with this file:', '继续处理此文件:'));
NEXT.filter(([t]) => t !== tool).slice(0, 3).forEach(([t, en, zh]) => {
const a = document.createElement('a');
- a.href = t === 'converter' && ZH ? '/zh/converter/' : '/' + t + '/';
+ a.href = (ZH && ZH_PAGES.includes(t) ? '/zh/' : '/') + t + '/';
a.textContent = T(en, zh);
a.addEventListener('click', async (e) => {
e.preventDefault();
@@ -185,16 +186,16 @@
'converter': { url: ZH ? '/zh/converter/' : '/converter/', name: T('Audio Converter', '音频转换器'),
kw: ['convert', 'format', 'mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'webm', 'mp4', 'video', 'extract audio',
'конверт', 'формат', 'преобраз', 'видео', '转换', '格式', '视频', '提取音频'] },
- 'audio-compressor': { url: '/audio-compressor/', name: T('Audio Compressor', '音频压缩'),
+ 'audio-compressor': { url: ZH ? '/zh/audio-compressor/' : '/audio-compressor/', name: T('Audio Compressor', '音频压缩'),
kw: ['compress', 'smaller', 'reduce', 'file size', 'shrink', 'bitrate', 'email', 'telegram', 'whatsapp',
'сжать', 'уменьш', 'размер', 'меньше', 'битрейт', '压缩', '变小', '缩小', '大小', '体积'] },
- 'volume-booster': { url: '/volume-booster/', name: T('Volume Booster', '音量增强'),
+ 'volume-booster': { url: ZH ? '/zh/volume-booster/' : '/volume-booster/', name: T('Volume Booster', '音量增强'),
kw: ['quiet', 'louder', 'volume', 'boost', 'normalize', 'amplify', 'gain',
'тих', 'громк', 'громч', 'усил', 'нормализ', '音量', '声音太小', '太小声', '放大', '增强', '标准化'] },
'audio-trimmer': { url: '/audio-trimmer/', name: T('Audio Trimmer', '音频剪辑'),
kw: ['trim', 'cut', 'crop', 'shorten', 'ringtone', 'clip', 'remove the start', 'remove the end',
'обрез', 'обреж', 'вырез', 'отрез', 'отреж', 'рингтон', 'укорот', '剪辑', '裁剪', '剪切', '铃声', '截取'] },
- 'audio-joiner': { url: '/audio-joiner/', name: T('Audio Joiner', '音频合并'),
+ 'audio-joiner': { url: ZH ? '/zh/audio-joiner/' : '/audio-joiner/', name: T('Audio Joiner', '音频合并'),
kw: ['join', 'merge', 'combine', 'concatenate', 'several files', 'multiple files',
'склеить', 'склей', 'объедин', 'соедин', 'несколько', '合并', '拼接', '连接', '多个'] },
'audio-speed-changer': { url: '/audio-speed-changer/', name: T('Speed Changer', '音频变速'),
@@ -203,7 +204,7 @@
'voice-to-text': { url: '/voice-to-text/', name: T('Voice to Text', '语音转文字'),
kw: ['transcri', 'to text', 'subtitle', 'speech to text', 'dictat', 'voice message', 'lecture', 'interview',
'расшифр', 'в текст', 'субтитр', 'голосов', 'лекци', 'диктов', '转文字', '转录', '字幕', '语音转', '听写'] },
- 'text-to-speech': { url: '/text-to-speech/', name: T('Text to Speech', '文字转语音'),
+ 'text-to-speech': { url: ZH ? '/zh/text-to-speech/' : '/text-to-speech/', name: T('Text to Speech', '文字转语音'),
kw: ['text to speech', 'read aloud', 'tts', 'voice over', 'narrat', 'speak',
'озвуч', 'вслух', 'синтез речи', 'прочитать', '朗读', '配音', '文字转语音', '语音合成'] },
};
diff --git a/audio-compressor/index.html b/audio-compressor/index.html
index da9b650..29aed3c 100644
--- a/audio-compressor/index.html
+++ b/audio-compressor/index.html
@@ -17,6 +17,9 @@
+
+
+
@@ -142,6 +145,7 @@
Text to SpeechVoice to TextAll tools
+ 中文
@@ -225,7 +229,7 @@
Audio Compressor
Free Online Audio Compressor
Audio Compressor reduces the file size of your audio by re-encoding it to MP3 at a bitrate and sample rate you choose. Perfect for fitting audio into email attachments, messaging apps, or storage limits.
How It Works
-
Lossy MP3 compression removes inaudible data to shrink the file. A 44 MB WAV can become a 4 MB MP3 at 128 kbps — roughly a 10x reduction with minimal quality loss for most uses. Higher bitrates (256–320 kbps) preserve more detail for music. Everything runs in your browser via the Web Audio API and a JavaScript MP3 encoder — nothing is uploaded.
+
Lossy MP3 compression removes inaudible data to shrink the file. A 44 MB WAV can become a 4 MB MP3 at 128 kbps — roughly a 10x reduction with minimal quality loss for most uses. Higher bitrates (256–320 kbps) preserve more detail for music. Everything runs in your browser, with the LAME MP3 encoder compiled to WebAssembly — nothing is uploaded.
Choosing the Right Bitrate
64 kbps — voice recordings and podcasts (smallest files). 128 kbps — general purpose, good quality for most uses. 192 kbps — high quality for music. 320 kbps — maximum quality, indistinguishable from uncompressed for most listeners. Lowering the sample rate to 22 kHz or 16 kHz shrinks speech files further.
Text to Speech marks online voices (for example Chrome's “Google” voices), which the browser vendor processes, so you can pick a voice that stays on your device.
Several files at once in the Audio Converter and the Audio Compressor: drop a batch, follow each file's progress, download them one by one or all as a ZIP.
Audio Converter saves M4A (AAC) and FLAC as well as MP3 and WAV, and takes video files (MP4, MOV, MKV, WebM) to pull out the sound.
The homepage helper understands settings: “compress to 10 MB”, “keep the first 30 seconds”, “1.5x faster”, “louder by 6 dB”, “convert to FLAC” (also in Russian and Chinese). It opens the tool with your file and those settings already in place.
diff --git a/converter/index.html b/converter/index.html
index 5da1ee6..0ad751a 100644
--- a/converter/index.html
+++ b/converter/index.html
@@ -18,6 +18,9 @@
+
+
+
@@ -32,9 +35,6 @@
-
-
-
diff --git a/llms.txt b/llms.txt
index c106112..755fc23 100644
--- a/llms.txt
+++ b/llms.txt
@@ -24,11 +24,11 @@
- [About AudioTools.dev](https://audiotools.dev/about/): who builds it, how files stay on the device (and how to verify it in DevTools), what the site loads from the network, and how it is built.
- [Changelog](https://audiotools.dev/changelog/): dated list of everything that has shipped since January 2026.
-- Chinese versions: [homepage](https://audiotools.dev/zh/), [audio converter](https://audiotools.dev/zh/converter/).
+- Chinese versions: [homepage](https://audiotools.dev/zh/), [audio converter](https://audiotools.dev/zh/converter/), [volume booster](https://audiotools.dev/zh/volume-booster/), [text to speech](https://audiotools.dev/zh/text-to-speech/), [audio joiner](https://audiotools.dev/zh/audio-joiner/), [audio compressor](https://audiotools.dev/zh/audio-compressor/).
## Key Facts
-- Privacy: all browser tools process audio locally. No files uploaded, audio is never collected. The site uses Google Analytics for anonymous page-view statistics only.
+- Privacy: all browser tools process audio locally. No files uploaded, audio is never collected. The site uses Google Analytics (and Yandex Metrika on Voice to Text) for anonymous page-view statistics only. Text to Speech uses the browser's voices; voices marked “online” are processed by the browser vendor.
- License: MIT.
- Formats: reads WAV, MP3, FLAC, OGG, AAC, M4A, WebM and video (MP4, MOV, MKV); the converter saves MP3, M4A, WAV or FLAC, the other tools MP3 or WAV.
- Works offline: Converter, Trimmer, Joiner, Volume Booster, Speed Changer, Compressor work without internet.
diff --git a/og/zh-audio-compressor.png b/og/zh-audio-compressor.png
new file mode 100644
index 0000000..6b591fa
Binary files /dev/null and b/og/zh-audio-compressor.png differ
diff --git a/og/zh-audio-joiner.png b/og/zh-audio-joiner.png
new file mode 100644
index 0000000..699b5a8
Binary files /dev/null and b/og/zh-audio-joiner.png differ
diff --git a/og/zh-text-to-speech.png b/og/zh-text-to-speech.png
new file mode 100644
index 0000000..3139f0a
Binary files /dev/null and b/og/zh-text-to-speech.png differ
diff --git a/og/zh-volume-booster.png b/og/zh-volume-booster.png
new file mode 100644
index 0000000..3a3933e
Binary files /dev/null and b/og/zh-volume-booster.png differ
diff --git a/scripts/build_zh.py b/scripts/build_zh.py
new file mode 100644
index 0000000..aa21736
--- /dev/null
+++ b/scripts/build_zh.py
@@ -0,0 +1,390 @@
+#!/usr/bin/env python3
+"""Build the Chinese tool pages from the English ones.
+
+ python3 scripts/build_zh.py && python3 scripts/seo_sync.py
+
+/zh// is // with the same code and translated text. Every English string in a table below must
+still be on the English page: if the English copy changes, the script stops and names the string to update,
+so a Chinese page never silently falls behind. After building, the script lists any English text left on a
+Chinese page. It also keeps the language links in step: hreflang and the 中文/EN switch on both versions,
+and the Chinese navigation and footer on every Chinese page.
+
+The Chinese converter (/zh/converter/) and home page (/zh/) are maintained by hand; only their navigation,
+footer and tool links are updated here.
+"""
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+SITE = 'https://audiotools.dev'
+ZH_PAGES = ['converter', 'volume-booster', 'text-to-speech', 'audio-joiner', 'audio-compressor'] # tools with a Chinese page
+BUILT = ['volume-booster', 'text-to-speech', 'audio-joiner', 'audio-compressor'] # built by this script
+
+ZH_HREF = lambda tool: f'/zh/{tool}/' if tool in ZH_PAGES else f'/{tool}/'
+
+
+def zh_nav(current, en_href):
+ items = [('converter', '转换器'), ('volume-booster', '音量增强'), ('text-to-speech', '文字转语音'), ('voice-to-text', '语音转文字')]
+ links = [f'{name}' for t, name in items]
+ links.append(f'全部工具')
+ links.append(f'EN')
+ return '
', re.S)
+FOOTER_RE = re.compile(r'', re.S)
+TOOL_HREF_RE = re.compile(r'href="/(' + '|'.join(ZH_PAGES) + r')/"')
+
+
+def english_page(tool):
+ """Add hreflang and the 中文 switch to the English page (idempotent); return its HTML."""
+ path = ROOT / tool / 'index.html'
+ s = path.read_text(encoding='utf-8')
+ s = ALT_RE.sub('', s)
+ s = re.sub(r'(\n)', lambda m: m.group(1) + alternates(tool), s, count=1)
+ s = re.sub(r'\s*]*>中文', '', s)
+ s = NAV_RE.sub(lambda m: m.group(0)[:-len('')].rstrip() + f'\n 中文\n ', s, count=1)
+ path.write_text(s, encoding='utf-8')
+ return s
+
+
+def build(tool, table):
+ s = english_page(tool)
+ missing = [en for en, _ in table if en not in s]
+ if missing:
+ sys.exit(f'{tool}: the English page no longer contains:\n ' + '\n '.join(missing))
+ table = table + [(en, zh) for en, zh in COMMON if en in s] # shared strings, where the page has them
+ for en, zh in sorted(table, key=lambda p: -len(p[0])): # longer strings first, so parts don't break them
+ s = s.replace(en, zh)
+ s = s.replace('', '', 1)
+ s = s.replace('aria-label="Main"', 'aria-label="主导航"', 1)
+ s = s.replace('', '')
+ s = s.replace(f'{SITE}/og/{tool}.png', f'{SITE}/og/zh-{tool}.png')
+ s = ALT_RE.sub('', s)
+ s = s.replace(f'{SITE}/{tool}/', f'{SITE}/zh/{tool}/') # canonical, og:url, JSON-LD url
+ s = re.sub(r'(\n)', lambda m: m.group(1) + alternates(tool), s, count=1)
+ s = re.sub(r'("@type":\s*"WebApplication",)(\s*)', lambda m: m.group(1) + m.group(2) + '"inLanguage": "zh-CN",' + (m.group(2) or ''), s, count=1)
+ s = TOOL_HREF_RE.sub(lambda m: f'href="/zh/{m.group(1)}/"', s) # links to tools that have a Chinese page
+ s = s.replace('href="/"', 'href="/zh/"')
+ s = NAV_RE.sub(lambda m: zh_nav(tool, f'/{tool}/'), s, count=1)
+ s = FOOTER_RE.sub(lambda m: zh_footer(tool), s, count=1)
+ out = ROOT / 'zh' / tool / 'index.html'
+ out.parent.mkdir(parents=True, exist_ok=True)
+ out.write_text(s, encoding='utf-8')
+ leftovers(out, s)
+
+
+ALLOWED = {'AudioTools.dev', 'AudioTools', 'GitHub', 'DevToolKit.site', 'Boosty', 'Roman Gordienko', 'MP3', 'WAV', 'FLAC', 'OGG',
+ 'M4A', 'AAC', 'WebM', 'ZIP', 'Web Audio API', 'Web Speech Synthesis API', 'Speech Synthesis API', 'dBFS', 'kbps', 'TTS', 'EN', 'Google', 'Chrome', 'Safari', 'Edge', 'Windows', 'macOS', 'Android', 'LAME', 'WebAssembly', 'PCM'}
+
+
+def leftovers(path, s):
+ """English words left in visible text, attributes and UI strings of a Chinese page."""
+ body = re.sub(r'', '', s[s.index('(.*?)', body, re.S))
+ text = re.sub(r'<[^>]+>', '\n', re.sub(r'', '', body, flags=re.S))
+ text += '\n'.join(re.findall(r'(?:title|placeholder|aria-label|alt)="([^"]*)"', body))
+ strings = [t for t in re.findall(r"'([^'\n]*)'", code) if re.search(r'[A-Za-z]{3}', t) and ' ' in t.strip() and not re.search(r'[<>={}()#]', t)]
+ words = set()
+ for chunk in list(text.split('\n')) + strings:
+ for w in re.findall(r'[A-Za-z][A-Za-z\-’\']{3,}', chunk):
+ if not any(w in a for a in ALLOWED):
+ words.add(w)
+ print(f'{path.relative_to(ROOT)}: built' + (f'; English left: {" ".join(sorted(words))}' if words else ''))
+
+
+def update_hand_made(path, current, en_href):
+ """Chinese navigation, footer and tool links on the hand-maintained Chinese pages."""
+ s = path.read_text(encoding='utf-8')
+ body_start = s.index('Drop audio file here or click to browse
', '
将音频文件拖放到此处,或点击选择
'),
+ ('
MP3, WAV, FLAC, OGG, M4A — processed locally, never uploaded