From 7976310232e5f7c3d4d4dbd5003cb3fdacdd3fb0 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 18 Apr 2026 08:46:25 +0200 Subject: [PATCH] add british english to supported languages --- PROXY_SETUP.md | 2 +- docker-compose.dev.yml | 3 +- docker-compose.prod.yml | 3 +- docker-compose.yml | 3 +- package-lock.json | 4 +- package.json | 2 +- ...ne-libretranslate-packages.cpython-312.pyc | Bin 0 -> 3812 bytes scripts/ensure-libretranslate-dirs.sh | 55 +++++++++++-- scripts/libretranslate-lt.default.env | 4 + scripts/prune-libretranslate-packages.py | 18 ++++- scripts/prune-libretranslate-packages.sh | 7 +- services/piper-tts-proxy/server.ts | 16 ++-- .../AdvancedEventLabDialog.tsx | 25 +++--- src/components/NoteOptions/DesktopMenu.tsx | 5 +- src/components/NoteOptions/useMenuActions.tsx | 11 +-- src/components/ui/dropdown-menu.tsx | 6 +- src/components/ui/select.tsx | 4 +- src/index.css | 46 +++++++++++ src/lib/language-display-meta.test.ts | 44 +++++++++++ src/lib/language-display-meta.ts | 73 ++++++++++++++++-- src/lib/languagetool-language-order.test.ts | 13 ++++ src/lib/languagetool-language-order.ts | 2 + src/lib/read-aloud.ts | 13 +++- src/lib/translate-client.ts | 58 +++++++++++++- src/lib/trinity-languages.test.ts | 20 +++-- src/lib/trinity-languages.ts | 28 ++++--- 26 files changed, 387 insertions(+), 78 deletions(-) create mode 100644 scripts/__pycache__/prune-libretranslate-packages.cpython-312.pyc create mode 100644 scripts/libretranslate-lt.default.env diff --git a/PROXY_SETUP.md b/PROXY_SETUP.md index 28e187e4..2c87ab9b 100644 --- a/PROXY_SETUP.md +++ b/PROXY_SETUP.md @@ -187,7 +187,7 @@ VITE_TRANSLATE_URL=/api/translate **Production:** `docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d` starts the full stack (including LanguageTool on **127.0.0.1:8010** and LibreTranslate on **127.0.0.1:5000**). Run `bash scripts/ensure-libretranslate-dirs.sh` once on the server (LibreTranslate UID **1032** on `.local-libretranslate`, Piper ONNX into `.local-piper-data` and the **`_piper-stack-data`** Docker volume when it exists). Proxy `/api/languagetool` and `/api/translate` from Apache/nginx to those ports, and bake the client with `LANGUAGE_TOOL_URL=/api/languagetool` and `TRANSLATE_URL=/api/translate` when running `./scripts/build-and-push-prod.sh`. -**Notes:** LanguageTool’s JVM image often needs **~1–2 GiB** RAM. LibreTranslate **does not listen on port 5000 until models are ready**; without **`LT_LOAD_ONLY`** it may pull **many gigabytes** first, so the Vite proxy can show **`ECONNRESET` on `/translate`** while booting. Compose defaults **`LT_LOAD_ONLY`** to **ten** widely used codes (**en, de, es, fr, it, pt, ru, zh, ja, ar** — see `libretranslate` in `docker-compose.dev.yml`). Override with **`LT_LOAD_ONLY`** to add or remove codes; first start downloads packs for every listed code. **`LT_UPDATE_MODELS`** defaults to **`true`** so if you **expand** `LT_LOAD_ONLY` later, a **recreated** container still **installs missing** Argos packages into the bind-mounted `.local-libretranslate` tree (otherwise an older en/de-only cache sticks). Set **`LT_UPDATE_MODELS=false`** after everything is installed if you want faster routine restarts. Models are stored under **`.local-libretranslate/share`** and **`.local-libretranslate/cache`** (gitignored) with **bind mounts** so they survive **`docker compose down`**, image updates, and container recreate. **`scripts/ensure-libretranslate-dirs.sh`** (run automatically by **`npm run dev:all`**, **`npm run stack:remote`**, **`npm run docker:editor-tools`**, etc.) creates those dirs and **`chown`s them to UID 1032** via a short **Alpine** container so the LibreTranslate user can write. If you start **`libretranslate` by hand**, run **`npm run docker:prep-libretranslate`** once first. First download can still take **several minutes**; use **`docker logs -f jumble-libretranslate`** until **`curl http://127.0.0.1:5000/languages`** returns JSON. If logs show **`Cannot update models`** / **`Unavailable language codes: …`**, one bad token in **`LT_LOAD_ONLY` aborts the whole install** (you stay on whatever was already on disk, often en/de only). **Norwegian** must be **`nb`** (Bokmål), not ISO **`no`**. After you shrink **`LT_LOAD_ONLY`**, run **`npm run docker:prune-libretranslate-packages`** to remove leftover Argos package dirs under **`.local-libretranslate/share/argos-translate/packages`** (and unused **MiniSBD** `.onnx` files); the script briefly stops **`jumble-libretranslate`** or **`imwald-libretranslate`**. Override codes with **`LT_LOAD_ONLY=…`** on the same command if they differ from the compose default. +**Notes:** LanguageTool’s JVM image often needs **~1–2 GiB** RAM. LibreTranslate **does not listen on port 5000 until models are ready**; without **`LT_LOAD_ONLY`** it may pull **many gigabytes** first, so the Vite proxy can show **`ECONNRESET` on `/translate`** while booting. Compose loads **`LT_LOAD_ONLY`** from **`scripts/libretranslate-lt.default.env`** (same file is read by **`scripts/ensure-libretranslate-dirs.sh`** and **`scripts/prune-libretranslate-packages.sh`**). Edit that file to add or remove codes, then recreate LibreTranslate; first start downloads packs for every listed code. For a one-off prune without editing the file, run **`export LT_LOAD_ONLY=…`** before **`npm run docker:prune-libretranslate-packages`**. **`LT_UPDATE_MODELS`** defaults to **`true`** so if you **expand** `LT_LOAD_ONLY` later, a **recreated** container still **installs missing** Argos packages into the bind-mounted `.local-libretranslate` tree (otherwise an older en/de-only cache sticks). Set **`LT_UPDATE_MODELS=false`** after everything is installed if you want faster routine restarts. Models are stored under **`.local-libretranslate/share`** and **`.local-libretranslate/cache`** (gitignored) with **bind mounts** so they survive **`docker compose down`**, image updates, and container recreate. **`scripts/ensure-libretranslate-dirs.sh`** (run automatically by **`npm run dev:all`**, **`npm run stack:remote`**, **`npm run docker:editor-tools`**, etc.) creates those dirs and **`chown`s them to UID 1032** via a short **Alpine** container so the LibreTranslate user can write. If you start **`libretranslate` by hand**, run **`npm run docker:prep-libretranslate`** once first. First download can still take **several minutes**; use **`docker logs -f jumble-libretranslate`** until **`curl http://127.0.0.1:5000/languages`** returns JSON. If logs show **`Cannot update models`** / **`Unavailable language codes: …`**, one bad token in **`LT_LOAD_ONLY` aborts the whole install** (you stay on whatever was already on disk, often en/de only). **Norwegian** must be **`nb`** (Bokmål), not ISO **`no`**. After you shrink **`LT_LOAD_ONLY`**, run **`npm run docker:prune-libretranslate-packages`** to remove leftover Argos package dirs under **`.local-libretranslate/share/argos-translate/packages`** (and unused **MiniSBD** `.onnx` files); the script briefly stops **`jumble-libretranslate`** or **`imwald-libretranslate`**. ## LibreTranslate (same-origin `/api/translate`) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e51311d5..5bdc6a75 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -67,8 +67,9 @@ services: - '5000:5000' # Without LT_LOAD_ONLY the image downloads many GB of models before binding :5000 — Vite then gets ECONNRESET on /translate. tty: true + env_file: + - ./scripts/libretranslate-lt.default.env environment: - LT_LOAD_ONLY: ${LT_LOAD_ONLY:-en,de,es,fr,it,pt,ru,zh,ja,ar} # Install missing packs when `LT_LOAD_ONLY` grows (persisted volume otherwise keeps old en/de-only index). LT_UPDATE_MODELS: ${LT_UPDATE_MODELS:-true} volumes: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index a3066480..64877235 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -83,8 +83,9 @@ services: ports: - '127.0.0.1:5000:5000' tty: true + env_file: + - ./scripts/libretranslate-lt.default.env environment: - LT_LOAD_ONLY: ${LT_LOAD_ONLY:-en,de,es,fr,it,pt,ru,zh,ja,ar} LT_UPDATE_MODELS: ${LT_UPDATE_MODELS:-true} volumes: # One tree under .local (same as upstream run.sh -v …/lt-local:/home/libretranslate/.local); avoids split-mount permission edge cases. diff --git a/docker-compose.yml b/docker-compose.yml index ada38407..ccfcd070 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,8 +49,9 @@ services: ports: - '5000:5000' tty: true + env_file: + - ./scripts/libretranslate-lt.default.env environment: - LT_LOAD_ONLY: ${LT_LOAD_ONLY:-en,de,es,fr,it,pt,ru,zh,ja,ar} LT_UPDATE_MODELS: ${LT_UPDATE_MODELS:-true} volumes: - ./.local-libretranslate:/home/libretranslate/.local diff --git a/package-lock.json b/package-lock.json index 30ad7113..a53bf307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.0.11", + "version": "23.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.0.11", + "version": "23.1.0", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 651f5fb4..10046fbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.0.11", + "version": "23.1.0", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/scripts/__pycache__/prune-libretranslate-packages.cpython-312.pyc b/scripts/__pycache__/prune-libretranslate-packages.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..119434e664733a6e1c11bc9d3d1b3cd577e868e6 GIT binary patch literal 3812 zcmcIm-A^0Y6~8kcd&by!0P~qBWcap~;)Fyg8yb=(n~uG}=<-Ee<Xx%Kq`{y2WAVOJ6@E)OeSd3oFr^YP*2qJ6+J7sJH+e!O^8`ebZB`02>FIj7cLCbeZ`$vGl;K`sw zP7=>!MD0W>QD!+*(rG-X1FN>^RqNDMN-8k`QrfUPSqpaCmOtgpd>vN*!_Luc#YShq z&(XQA$XW7`>yN-+C+UpdsK#V*xKcyNPZR0HEN(uAg59LMeAbgk#CTYZnn`D+M(w2> zwUEyD^9rj>^p@qlN=dsF?|V6Vz?uCg;#Si30^znVZB?mR)_*LikCV9im`GRXq^e0+ zrkXUZU_zzS;nIpe71pINC6c1*l0pb3Sej6&G;%*QGIr%!Xl!)k=aO$q)pRMYPAj@5 z=~FWNs8S#hIIT^A>Yk;3orV=H7S@SB@Wu>#)Xx~)ov=P-I4IGlsS>Fk1|d7V*x}h%U*B)!$ zN6`%@C1D)W$rIhE(GoFPkI4$r47;XNIRQ(0nCjY$3^1~nwhi|y)b0Z!-gY z97>otv(fO3!Ikt)uaYF~`m_j`(Y4bFI<1iY17}WzBagz9L<=NljmGlnLoq!RQz0Z( ziOo{;5;p$`XupG>1@u?Z`|F#}ezhs~tcyJbv2Rlhtc!vCz@2sR&ce;2*s>{htcx9M zgL$!|AfDS4FRhE03gVC7BD;McWh;seE3@g@XHQeymgvpstDijoB&Th(oGQ8-i!O2H za{6-i@@vESoa1B-ZOu3pV@YSwST#ht0mty@XGh< zSia?Ap=LPG4evmd$Z+p3$1WooXqWjxvSI(KMB)j3*6dKTF4XK3qvp!^jj?;7@v*V{ zNylDO8-kT0gOAIKtUbgAr-b7qQt4OFgnvPMz5GvL_V;qM(#x<>elRnKEHsU6@-(hkrzn7C$IktEt|zSbwgG7U{oH6DU#Zq|ar zzcplvYUYFmkP0;`LR!ezpIWxH-&l{6kW*j_%z$W%R&`J)kPe zlQ;i<_wHR~w~Q#*bzoH?k}v6$_7t(--)r!p5LP2FGgwul-Qd!2Xj9X=95Za%tY+|L z&zlWhnlLt)7o~26O`;n-31y!dSWKNE*sv$43=_zq>2M)Z zgCkF5-LRMLMT09%6fUaD3h<96HG_+r=(p?B31}EI<)&HlZiPizgIa(gmFn=y$7C=m z({lY11~XwW;D^R_N{GQuDCW?@dsB^r=?c?u&(t4KDYvCc?+O>=DxQv!OY}B)GaF93 z21wYpF%0t$l>7!cOP|_rP|e>^>$XE+E@dM-2*`KM$mJ<=j-^|Rx0Xg1M>Ch!y9Ntg z7Yf|aCU z=F{`p`x{~>%-AjWi7YO-yHZTiqq2*pxG~ Nc}K@<)LBB=e*n`GRto?C literal 0 HcmV?d00001 diff --git a/scripts/ensure-libretranslate-dirs.sh b/scripts/ensure-libretranslate-dirs.sh index 886591d4..83a2340a 100755 --- a/scripts/ensure-libretranslate-dirs.sh +++ b/scripts/ensure-libretranslate-dirs.sh @@ -1,19 +1,23 @@ #!/usr/bin/env bash # One-shot host prep for editor stack sidecars: # 1) LibreTranslate bind mount — dirs + ownership UID 1032 (official image user). -# 2) Piper ONNX voices — trinity + read-aloud extras (rhasspy/piper-voices) into: -# - ./.local-piper-data (dev compose bind mount), and -# - Docker volume _piper-stack-data when it exists (docker-compose.prod.yml Wyoming /data). +# 2) Recommended Argos / LT_LOAD_ONLY list — written beside the volume (see below). +# 3) Piper ONNX voices — same set as src/lib/trinity-languages.ts + piper-tts-proxy voiceMap +# into ./.local-piper-data and optionally Docker volume _piper-stack-data. +# +# Argos translation models are pulled by the LibreTranslate **container** from LT_LOAD_ONLY on first +# start (bind mount .local-libretranslate). Default list: scripts/libretranslate-lt.default.env (same file +# as docker-compose `env_file` for libretranslate). # # Piper download logic lives in this file so you can copy **only** this script to a server and run it # from the repo root (still need curl, docker; full clone is easier: bash scripts/ensure-libretranslate-dirs.sh). # # Internal entry: bash ensure-libretranslate-dirs.sh --download-piper-only [DEST] -# (used by scripts/download-piper-extra-voices.sh — keep voice list in sync with trinity-languages.ts) +# (used by scripts/download-piper-extra-voices.sh — keep Piper relpaths in sync with trinity-languages.ts) # # Optional env: # COMPOSE_PROJECT_NAME — Docker Compose project name (default: basename of repo dir), for volume *_piper-stack-data. -# SKIP_PIPER_VOICES=1 — only fix LibreTranslate permissions, do not download Piper. +# SKIP_PIPER_VOICES=1 — only fix LibreTranslate permissions (+ LT_LOAD_ONLY hint file), do not download Piper. # HF_BASE — Hugging Face resolve base for Piper ONNX (default rhasspy/piper-voices/main). set -euo pipefail @@ -26,7 +30,21 @@ _resolve_root() { fi } -# Keep in sync with src/lib/trinity-languages.ts (TRINITY_PIPER_VOICE + EXTRA_READ_ALOUD_PIPER_VOICE) and server voiceMap. +load_stack_lt_load_only() { + local f="${ROOT}/scripts/libretranslate-lt.default.env" + [[ -f "$f" ]] || { + echo "[ensure] Missing ${f}" >&2 + exit 1 + } + STACK_LT_LOAD_ONLY="$(grep -E '^[[:space:]]*LT_LOAD_ONLY=' "$f" | head -1 | sed 's/^[[:space:]]*LT_LOAD_ONLY=//')" + [[ -n "$STACK_LT_LOAD_ONLY" ]] || { + echo "[ensure] LT_LOAD_ONLY empty in ${f}" >&2 + exit 1 + } +} + +# Keep in sync with src/lib/trinity-languages.ts (TRINITY_PIPER_VOICE + EXTRA_READ_ALOUD_PIPER_VOICE) +# and services/piper-tts-proxy/server.ts getVoiceForLanguage voiceMap. download_piper_voices_to() { local dest="${1:?destination directory}" local hf="${HF_BASE:-https://huggingface.co/rhasspy/piper-voices/resolve/main}" @@ -34,6 +52,7 @@ download_piper_voices_to() { local relpath base_name onnx json for relpath in \ "en/en_US/lessac/medium/en_US-lessac-medium" \ + "en/en_GB/alan/medium/en_GB-alan-medium" \ "de/de_DE/thorsten/medium/de_DE-thorsten-medium" \ "fr/fr_FR/siwis/medium/fr_FR-siwis-medium" \ "es/es_ES/davefx/medium/es_ES-davefx-medium" \ @@ -61,6 +80,18 @@ download_piper_voices_to() { echo "Piper ONNX done → ${dest}" } +write_lt_load_only_hint() { + local lt_dir="${1:?libretranslate data dir}" + local hint="${lt_dir}/.recommended-lt-load-only.txt" + docker run --rm \ + -e STACK_LT_LOAD_ONLY="$STACK_LT_LOAD_ONLY" \ + -v "$lt_dir:/d" \ + alpine:3.20 \ + sh -c 'printf "%s\n" "$STACK_LT_LOAD_ONLY" > /d/.recommended-lt-load-only.txt && chown 1032:1032 /d/.recommended-lt-load-only.txt' + echo "[ensure] Recommended LibreTranslate LT_LOAD_ONLY (Argos) → ${hint}" + echo " Keep in sync with scripts/libretranslate-lt.default.env (compose env_file). Recreate libretranslate once (LT_UPDATE_MODELS=true) after changing the list." +} + if [[ "${1:-}" == "--download-piper-only" ]]; then shift _resolve_root @@ -69,17 +100,28 @@ if [[ "${1:-}" == "--download-piper-only" ]]; then fi _resolve_root +load_stack_lt_load_only PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$ROOT")}" PIPER_VOL="${PROJECT}_piper-stack-data" echo "[ensure] LibreTranslate data dir (UID 1032) …" +if [[ -e "$ROOT/.local-libretranslate" ]] && [[ ! -w "$ROOT/.local-libretranslate" ]]; then + echo "[ensure] Resetting bind-mount ownership so the host can create dirs (final step sets UID 1032) …" + docker run --rm \ + -v "$ROOT/.local-libretranslate:/d" \ + alpine:3.20 chown -R "$(id -u):$(id -g)" /d +fi mkdir -p "$ROOT/.local-libretranslate/share" "$ROOT/.local-libretranslate/cache" + +write_lt_load_only_hint "$ROOT/.local-libretranslate" + docker run --rm \ -v "$ROOT/.local-libretranslate:/d" \ alpine:3.20 chown -R 1032:1032 /d if [[ "${SKIP_PIPER_VOICES:-}" == "1" ]]; then echo "[ensure] SKIP_PIPER_VOICES=1 — skipping Piper voice download." + echo "[ensure] Stack languages: translate=${STACK_LT_LOAD_ONLY} (LibreTranslate); grammar=LanguageTool; read-aloud=Piper in .local-piper-data (run again without SKIP to fetch)." exit 0 fi @@ -98,3 +140,4 @@ else fi echo "[ensure] Done." +echo "[ensure] Summary — translate: LT_LOAD_ONLY=${STACK_LT_LOAD_ONLY} in Compose; grammar: LanguageTool; read-aloud: Piper ONNX above (+ Wyoming)." diff --git a/scripts/libretranslate-lt.default.env b/scripts/libretranslate-lt.default.env new file mode 100644 index 00000000..e65b5f87 --- /dev/null +++ b/scripts/libretranslate-lt.default.env @@ -0,0 +1,4 @@ +# Single source for LibreTranslate Argos codes (LT_LOAD_ONLY). +# Referenced by: docker-compose*.yml (libretranslate env_file), scripts/ensure-libretranslate-dirs.sh, +# scripts/prune-libretranslate-packages.{sh,py}. Override for one-off runs: export LT_LOAD_ONLY=… before compose/prune. +LT_LOAD_ONLY=en,de,es,fr,it,pt,ru,zh,ar,nl,pl,cs,tr diff --git a/scripts/prune-libretranslate-packages.py b/scripts/prune-libretranslate-packages.py index 986a65c2..e67f6ce0 100755 --- a/scripts/prune-libretranslate-packages.py +++ b/scripts/prune-libretranslate-packages.py @@ -4,9 +4,23 @@ import os import re import shutil import sys +from pathlib import Path -DEFAULT = "en,de,es,fr,it,pt,ru,zh,ja,ar" -allowed = set(os.environ.get("LT_LOAD_ONLY", DEFAULT).replace(" ", "").split(",")) + +def default_lt_load_only() -> str: + env_path = Path(__file__).resolve().parent / "libretranslate-lt.default.env" + for raw in env_path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("LT_LOAD_ONLY="): + return line.split("=", 1)[1].strip() + raise RuntimeError(f"LT_LOAD_ONLY not set in {env_path}") + + +allowed = set( + os.environ.get("LT_LOAD_ONLY", default_lt_load_only()).replace(" ", "").split(",") +) if not allowed or allowed == {""}: print("LT_LOAD_ONLY empty", file=sys.stderr) sys.exit(1) diff --git a/scripts/prune-libretranslate-packages.sh b/scripts/prune-libretranslate-packages.sh index 9f8c81fd..8c66e018 100755 --- a/scripts/prune-libretranslate-packages.sh +++ b/scripts/prune-libretranslate-packages.sh @@ -1,10 +1,13 @@ #!/usr/bin/env bash -# Drop Argos packages / MiniSBD files not in LT_LOAD_ONLY (default matches docker-compose libretranslate). +# Drop Argos packages / MiniSBD files not in LT_LOAD_ONLY (default: scripts/libretranslate-lt.default.env). # Stops LibreTranslate briefly so files are not in use; uses Docker+Alpine to delete UID-1032-owned trees. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" ARGOS="$ROOT/.local-libretranslate/share/argos-translate" -LT_LOAD_ONLY="${LT_LOAD_ONLY:-en,de,es,fr,it,pt,ru,zh,ja,ar}" +LT_DEFAULT_ENV="$ROOT/scripts/libretranslate-lt.default.env" +if [[ -z "${LT_LOAD_ONLY:-}" ]]; then + LT_LOAD_ONLY="$(grep -E '^[[:space:]]*LT_LOAD_ONLY=' "$LT_DEFAULT_ENV" | head -1 | sed 's/^[[:space:]]*LT_LOAD_ONLY=//')" +fi if [[ ! -d "$ARGOS/packages" ]]; then echo "Nothing to prune (missing $ARGOS/packages)." >&2 diff --git a/services/piper-tts-proxy/server.ts b/services/piper-tts-proxy/server.ts index b0304f44..f70a1463 100644 --- a/services/piper-tts-proxy/server.ts +++ b/services/piper-tts-proxy/server.ts @@ -986,8 +986,10 @@ function detectLanguage(text: string): string { const italianChars = (sample.match(/[àèéìòùÀÈÉÌÒÙ]/g) || []).length; // Russian/Cyrillic const cyrillicChars = (sample.match(/[а-яёА-ЯЁ]/g) || []).length; - // Chinese/Japanese/Korean (CJK) - const cjkChars = (sample.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length; + // CJK scripts: Hangul / kana → English Piper (no ko/ja models); Han → Chinese when dominant. + const hangulChars = (sample.match(/[\uac00-\ud7af]/g) || []).length; + const kanaChars = (sample.match(/[\u3040-\u309f\u30a0-\u30ff]/g) || []).length; + const hanChars = (sample.match(/[\u4e00-\u9fff]/g) || []).length; // Arabic const arabicChars = (sample.match(/[\u0600-\u06ff]/g) || []).length; @@ -998,12 +1000,15 @@ function detectLanguage(text: string): string { const spanishRatio = spanishChars / total; const italianRatio = italianChars / total; const cyrillicRatio = cyrillicChars / total; - const cjkRatio = cjkChars / total; + const hangulRatio = hangulChars / total; + const kanaRatio = kanaChars / total; + const hanRatio = hanChars / total; const arabicRatio = arabicChars / total; // Detect based on highest ratio if (cyrillicRatio > 0.1) return 'ru'; - if (cjkRatio > 0.1) return 'zh'; // Default to Chinese for CJK + if (hangulRatio > 0.06 || kanaRatio > 0.02) return 'en'; + if (hanRatio > 0.1) return 'zh'; if (arabicRatio > 0.1) return 'ar'; if (germanRatio > 0.02) return 'de'; if (frenchRatio > 0.02) return 'fr'; @@ -1027,6 +1032,7 @@ function getVoiceForLanguage(lang: string): string { // Voice map keys / ids: keep in sync with `src/lib/trinity-languages.ts` (`TRINITY_PIPER_VOICE`, `EXTRA_READ_ALOUD_PIPER_VOICE`). const voiceMap: Record = { 'en': 'en_US-lessac-medium', // Default English voice + 'en-gb': 'en_GB-alan-medium', // British English (rhasspy/piper-voices; install via scripts/download-piper-extra-voices.sh) 'de': 'de_DE-thorsten-medium', // German 'fr': 'fr_FR-siwis-medium', // French 'es': 'es_ES-davefx-medium', // Spanish @@ -1039,8 +1045,6 @@ function getVoiceForLanguage(lang: string): string { 'nl': 'nl_NL-mls-medium', // Dutch 'cs': 'cs_CZ-jirka-medium', // Czech 'tr': 'tr_TR-dfki-medium', // Turkish - // 'ja': 'ja_JP-nanami-medium', // Japanese - not available - // 'ko': 'ko_KR-kyungha-medium', // Korean - not available }; return voiceMap[lang] || voiceMap['en']; // Fall back to English diff --git a/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx b/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx index 0a971d3a..0eefc7ed 100644 --- a/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx +++ b/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx @@ -20,8 +20,7 @@ import { isLanguageToolConfigured } from '@/lib/languagetool-client' import { languageToolLintExtension, requestAdvancedLabGrammarLint } from '@/lib/languagetool-cm-linter' import { pickLanguageToolCodeForTranslateTarget } from '@/lib/languagetool-language-order' import { - filterTranslateLanguagesWithGrammarCatalog, - TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS, + buildResolvedTranslateMenuLanguageOptions, translateLanguageOptionMatchesQuery } from '@/lib/language-display-meta' import { LanguageSelectOptionLines } from '@/lib/language-select-option-lines' @@ -31,6 +30,7 @@ import { translateAdvancedLabMarkup } from '@/lib/advanced-lab-markup-protect' import { fetchTranslateLanguages, isTranslateConfigured, + translateApiLanguageCode, type TranslateLanguageOption } from '@/lib/translate-client' import { setReadAloudTranslationForEvent } from '@/lib/read-aloud-translation-override' @@ -537,9 +537,7 @@ export default function AdvancedEventLabDialog({ void fetchTranslateLanguages() .then((list) => { if (cancelled) return - const base = list.length > 0 ? list : TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS - const filtered = filterTranslateLanguagesWithGrammarCatalog(base) - const resolved = filtered.length > 0 ? filtered : TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS + const resolved = buildResolvedTranslateMenuLanguageOptions(list) if (!resolved.length) { setTranslateLangs([]) setTranslateLoad('empty') @@ -547,17 +545,17 @@ export default function AdvancedEventLabDialog({ } setTranslateLangs([...resolved]) setTranslateSource('auto') - const codes = resolved.map((l) => l.code) + const codes = resolved.map((l: TranslateLanguageOption) => l.code) const tgt = codes.includes('en') ? 'en' : codes[0]! setTranslateTarget(tgt) setTranslateLoad('ready') }) .catch(() => { if (cancelled) return - const resolved = TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS + const resolved = buildResolvedTranslateMenuLanguageOptions([]) setTranslateLangs([...resolved]) setTranslateSource('auto') - const codes = resolved.map((l) => l.code) + const codes = resolved.map((l: TranslateLanguageOption) => l.code) setTranslateTarget(codes.includes('en') ? 'en' : codes[0]!) setTranslateLoad('ready') }) @@ -769,7 +767,10 @@ export default function AdvancedEventLabDialog({ const text = markupView.current?.state.doc.toString() ?? sliceRef.current?.content ?? '' if (!text.trim()) return if (translateLoad !== 'ready' || translateLangs.length === 0) return - if (translateSource !== 'auto' && translateSource === translateTarget) { + if ( + translateSource !== 'auto' && + translateApiLanguageCode(translateSource) === translateApiLanguageCode(translateTarget) + ) { toast.message(t('Advanced lab translation same source target')) return } @@ -851,7 +852,7 @@ export default function AdvancedEventLabDialog({ aria-label={t('Language list filter placeholder')} /> -
+
{ltListFiltered.map((code) => (
-
+
{showTranslateSourceAuto ? ( {t('Advanced lab translation source auto')} ) : null} @@ -955,7 +956,7 @@ export default function AdvancedEventLabDialog({ aria-label={t('Language list filter placeholder')} />
-
+
{translateLangsFilteredTgt.map((l) => ( {action.label} - + {action.subMenuSearchable ? (
{ if (cancelled) return - const base = list.length > 0 ? list : TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS - const filtered = filterTranslateLanguagesWithGrammarCatalog(base) - setTranslateMenuOptions( - filtered.length > 0 ? filtered : TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS - ) + setTranslateMenuOptions(buildResolvedTranslateMenuLanguageOptions(list)) }) .catch(() => { - if (!cancelled) setTranslateMenuOptions(TRANSLATE_GRAMMAR_LANGUAGE_OPTIONS) + if (!cancelled) setTranslateMenuOptions(buildResolvedTranslateMenuLanguageOptions([])) }) return () => { cancelled = true diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 6b4fab5b..72ead4b8 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -55,7 +55,7 @@ const DropdownMenuSubContent = React.forwardRef< DropdownMenuSubContentPositionProps & { showScrollButtons?: boolean } ->(({ className, showScrollButtons = true, side: sideProp, align: alignProp, ...props }, ref) => { +>(({ className, showScrollButtons = false, side: sideProp, align: alignProp, ...props }, ref) => { const [canScrollUp, setCanScrollUp] = React.useState(false) const [canScrollDown, setCanScrollDown] = React.useState(false) const contentRef = React.useRef(null) @@ -143,7 +143,7 @@ const DropdownMenuSubContent = React.forwardRef<
{props.children} @@ -245,7 +245,7 @@ const DropdownMenuContent = React.forwardRef<
{props.children} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index d35b5bce..2e4fcc3d 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -78,17 +78,15 @@ const SelectContent = React.forwardRef< position={position} {...props} > - {children} - ) diff --git a/src/index.css b/src/index.css index 6b73285d..1f416802 100644 --- a/src/index.css +++ b/src/index.css @@ -70,6 +70,52 @@ display: none; /* Safari and Chrome */ } + /* Popover / select lists: keep a visible vertical scrollbar (not overlay-only). */ + .popover-scroll-y { + overflow-y: scroll; + scrollbar-gutter: stable; + scrollbar-width: thin; + } + .popover-scroll-y::-webkit-scrollbar { + width: 10px; + } + .popover-scroll-y::-webkit-scrollbar-thumb { + border-radius: 9999px; + background-color: hsl(var(--muted-foreground) / 0.35); + } + .popover-scroll-y::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--muted-foreground) / 0.5); + } + .popover-scroll-y::-webkit-scrollbar-track { + border-radius: 9999px; + background-color: hsl(var(--muted) / 0.45); + } + + /* + * Radix Select injects a sibling