Skip to content

Translation: panel in the channel menu, quieter lines, fastText + ELD language identification - #12

Closed
rdrake wants to merge 17 commits into
developfrom
claude/project-thread-ug2j7w
Closed

rdrake wants to merge 17 commits into
developfrom
claude/project-thread-ug2j7w

Conversation

@rdrake

@rdrake rdrake commented Nov 8, 2026

Copy link
Copy Markdown
Owner

The translation redesign from claude/project-thread-f2nesd, merged onto develop. Until now it lived only on that side branch, so builds cut from develop left it out.

What

  • Translation panel: a header popover like the mentions one, opened from the channel menu. It replaces the button that sat beside the channel name.
  • Composer translation: a live preview, one-step send, and a language sheet with a suggestion card. The language you pick sticks.
  • Chat lines: no "=" or "?" tags, and no mark on a line that's already in the reading language. A translation shows in the original's place, and a tap on the text flips it back. Unplaced and guessed-at lines are left alone.
  • Language identification: fastText and ELD replace the old detector, and an English channel no longer drifts to Polish.
  • Downloads: reading never asks before a small download.

Merge notes

Testing

pnpm lint, pnpm typecheck, the production build and mocha (5101 passing) all pass.


Generated by Claude Code

rdrake added 17 commits November 7, 2026 20:26
…e channel menu

The mentions pane's look becomes one shared set of classes, so a pane
under a header button is built from it instead of styled again:
.header-popover-container (the v-backdrop over #viewport),
.header-popover (the surface; the whole chat under the header on a phone),
.header-popover-title with its .header-popover-aside and
.header-popover-close, placed by helpers/headerPopover.ts placeUnder. The
themes that styled .mentions-popup style .header-popover instead.

TranslationPanel.vue is now one of these: under the channel menu's ⋮,
the same title row as the mentions, the sections and hints in every
layout, formality as the segmented control everywhere, controls 2.25rem
with a pointer and 2.75rem under a finger. It closes through its own
escape layer (so Android's Back closes it too), keeps Tab inside with
trapTab, and a press outside closes it and does what the header button
there says, as the mentions pane does. The segmented control no longer
leaves an empty third cell under two choices, and takes the pane's own
colours rather than the composer's (black on black on zelenzy).

The channel header's translate button is gone: "Translation…" in the
channel menu is the one way in, and the panel's switch is reading's
on/off. Its nine tooltip strings leave the pot and the catalogs.

Context menu icons get a 1.5rem box with the glyph centred, so the
language glyph (a full 1.25em) no longer runs into its label.

Docs: client-features.md § Header popovers, translation.md § The
channel's panel. The translation scenarios open the panel through the
menu (lib/translation.mjs openTranslationPanel, READING_ON).
- Escape closes the panel while the focus is in one of its fields too: the
  app's binding ignores keys typed in a field, so the pane takes Escape
  itself (@keydown.esc, as SourceLanguagePicker.vue does).
- A header popover never runs past the bottom of #viewport, the visible
  band, so a short window or iOS's keyboard cannot push the panel's
  Settings link out of reach.
- An engine's pause showed only in the removed header button's label:
  the panel now says "Translation is paused for now: <reason>" while it
  lasts (translate.panel.paused).
- The chosen segment is marked by a ring with the label in the body's
  ink; link-coloured text on its own wash fell under 4.5:1 on light
  themes. Increase Contrast rings it in the body colour.
- Settings' intro no longer sends people to the header button
  (translate.settings.intro, fuzzy in every catalog until retranslated);
  translate.unavailable.lead's context and references drop Chat.vue; the
  catalogs' references follow the pot (merge.ts).
- The panel is aria-modal, as the other dialogs that trap Tab are, and is
  exempt from .screen-overlay like the mentions pane (its phone rule is
  .header-popover's own).
- translate-reading.mjs: three controls share a width, the hints show,
  the reading switch is picked by place (the interface is French by then).
- Stale comments about the old sheet; logical border on the foot.
…ick sticks

With a send target set, a pause in typing previews the translation in the
strip and Return sends it; a Return before the preview is done waits for it.
The translate button names the target (EN → PT) instead of a placeholder
that wrapped on a phone, the strip loses its Send and Edit buttons, and a
translated line's label is a quiet "from Portuguese" ("sent in" for your
own) under the text.

The source picked with the channel's target is kept (writeFrom) and used
for every draft instead of guessing; a guess with no route here falls back
to the reading language. The language dialog no longer focuses its select
on a touch screen (iOS opened its list unasked), docks to the chat pane and
closes when the sidebar opens.
A Return waiting on a preview that ends with no strip (the draft needs no
translation, or the target moved) is answered once instead of dropped; a
waiting send is keyed on the whole draft, so adding or removing /me cancels
it, and it no longer pulls the focus back. Only real typing previews, not a
channel switch or ArrowUp. The source picked with the target still lets a
draft typed in the target go as typed, and is cleared with the target or
when the panel sets one. The button's EN → PT and the read-back's colon come
from the catalog, and the strip's label is no longer upper-cased.
The translate button now opens a language sheet in place of the two
selects: I write / They read with a swap, the languages the channel has
been written in lately as chips, a search by either name, and every
language with what picking it costs (its model's download size, Corrects
spelling, or Not available here with the reason on a tap). Picking under
They read applies at once; Off turns the target off.

A channel whose recent lines are mostly in another language offers to
write in it on a card above the composer, declined per channel
(channelStore noSuggest).
A header with the title, Turn off and a close button. Languages nothing
here can translate into go last under one heading instead of a label on
every row, and the pair and rows are slimmer.
…g on

The strip loses its filled box: a thin rule, muted label, smaller text
and read-back. Picking a send language in another language also switches
the channel's reading on, since the replies come back in it.
In a channel that writes the reader's language, an "English" chip on
every line was noise. Only a line the detector could not place keeps its
"?" mark; the toolbar's Translate still translates either.
A translated line's "from German" floats at the end of its first line
instead of taking a row, and the composer preview's label and Copy sit
beside the text with the read-back on one line. An own line the reader
got back unchanged shows no "=" chip and no "sent in" label.
A line detection skipped (unplaceable or already in the reading
language) and a line the engine handed back unchanged now render nothing
under the message. Both were noise on short chat lines; the toolbar's
Translate still translates any of them.
The reader translated every line the detector could not place, with the
source left to the engine, and a channel declaring the reading language
plus one other placed every short line in the other by declaration. On
an English channel that read "lol" and "Yeah?" as Polish.

Now a line is left alone when it cannot be placed, or is placed only on
a weak hint or thin evidence (the short-line word lookup, or franc on
fewer than five words) in a language the channel is not known to write
(its declared languages and prior). Thin verdicts no longer feed the
prior, and the declared-language shortcut for short lines needs exactly
one declared language that is not the reading one. The toolbar's
Translate still translates any skipped line.
The channel prior learned only from franc verdicts, never from the
function-word classifier that places most English, so a few of the
user's own Polish sends made an English channel's prior Polish, and
that prior then broke English/Polish ties as Polish ("I know what to do
tho" read as Polish).

The prior now learns from any confident verdict on someone else's line
(not weak, thin, prior-settled or too short; never the user's own). A
tie the prior broke against the reading language is left alone. A long
line franc cannot place, or only leans toward, still translates when
the reading language is not among its contenders; short ones do not.

The composer's translate button now shows while the device probe is
still running, instead of appearing when it lands and shifting the
input on every app load.
A done translation now stands where the original message stood, with a
small flipper before it (TranslationFlip.vue) that swaps the original
back in one tap, like the header topic's flipper. The second row under
the message and its "from …" label are gone; the flipper's tooltip says
where the line came from. Nothing renders while a line is pending, so
it swaps in once instead of streaming into a row that then vanishes.
A failure keeps its row with the reason and retry.

The translated line's menu (show original, copy, retranslate, from
another language) moves to the message toolbar's Translate.
franc, the function-word classifier and the 800-word lookup placed
"uncanny" and "I know what to do tho" in Polish. Two identifiers now
read each line, fastText's lid.176 and ELD, and a line is placed only
where they agree, or where one of them stands alone with the reading
language nowhere among fastText's top three. A line neither is sure of
is left alone.

On 339 hand-labelled chat lines (tools/translate-eval/lid.mts), 1 English
line in 166 is placed in another language (it was 7), and 83% of the
foreign lines are placed right (it was 47%). The identifiers load in their
own chunk, js/lid.js, and the model and wasm come from js/lid/.
The fa-language icon before a translated line was the only thing that
swapped the translation and the original. Now a tap on the text does it,
and the icon is just a marker that shows which one is up. A tap on a
link, nick, channel or spoiler inside the line still goes to that, and
so does the end of a selection or long press and the tap that closes an
open toolbar. The toolbar's menu keeps the swap for keyboard users.
When reading needed a model that wasn't on the device, it opened the
download dialog, and on iOS it opened the system's language-pack sheet
as soon as a new language turned up. Now reading asks nothing:

- A copy of 128 MB or less (an OPUS-MT pair, a deploy's per-language
  models) downloads without asking, unless the connection reports
  itself metered.
- When reading needs a bigger model, the line shows "Needs a <size>
  download · Download" in place of the failure. The tap is the yes,
  and the line retries with the download's progress shown on it.
- On iOS, reading takes only packs that are already installed. The
  table translates the line now, and the first line translated from
  that pair shows "Apple can translate <language> on this device · Get
  it", which opens the sheet.

The composer, which the user is actively waiting on, still asks in the
dialog and still offers the sheet.
…to claude/project-thread-ug2j7w

# Conflicts:
#	client/js/translate/service.ts
@rdrake

rdrake commented Nov 8, 2026

Copy link
Copy Markdown
Owner Author

Superseded by evilnet#224.

This branch was successfully deployed

1 active deployment
github-pages — bc4a0b00 Deployed Nov 8, 2026 by rdrake via deploy #184
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant