Skip to content

Add compose-side translation to editor#3325

Merged
feruzm merged 2 commits into
developmentfrom
feature/compose-translation
Jul 6, 2026
Merged

Add compose-side translation to editor#3325
feruzm merged 2 commits into
developmentfrom
feature/compose-translation

Conversation

@feruzm

@feruzm feruzm commented Jul 6, 2026

Copy link
Copy Markdown
Member

Mobile counterpart of ecency/vision-next#1080: a Translate action in the editor toolbar that machine-translates the author's own writing and appends it under a divider with a target-language heading, matching the common bilingual posting format.

What

  • New translate toolbar button opens a COMPOSE_TRANSLATE sheet: franc-based source detection (same sampling as the reader gate), source/target dropdowns from the LibreTranslate language list, progress indicator, preview with RTL support, and an optional [ES | EN] title marker (skipped when it would exceed the title limit).
  • New translateMarkdown (ported from web) translates block-aware while preserving structure: code fences, images, tables, raw HTML, bare URLs and horizontal rules pass through untouched; heading/quote/list markers are re-attached; indented continuation lines keep their whitespace; plain blocks batch per request. Any failure rejects the whole run and closing the sheet cancels the request chain.
  • Changing source or target clears a pending result so a translation can never be applied under the wrong language heading.
  • Apply appends through the same programmatic write the AI assist flow uses; nothing auto-publishes.

Notes

  • Kept separate from the AI assist sheet since translation is unbilled (LibreTranslate) while AI assist is Points-priced.
  • languageDisplayName gained an optional locale argument (1-arg behavior unchanged, covered by an added test).
  • 28 new unit tests for the translator; scoped suites 46/46 green, full suite passes, eslint delta clean.

Summary by CodeRabbit

  • New Features

    • Added a translate-in-compose flow for drafts, including a new modal, toolbar action, translated preview, progress updates, and an option to apply the translated text back into the editor.
    • Added localized labels and messages for translation settings, detected language, errors, and publishing reminders.
  • Bug Fixes

    • Improved markdown translation handling so formatting, code blocks, tables, images, and other special content are preserved more reliably.
    • Added localized language display names for more accurate language labeling in the UI.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a compose-time "Translate post" feature: a new ComposeTranslateModal sheet that detects source language, translates draft markdown via a new markdown-aware translateMarkdown engine, and applies the translated text back into the editor. Includes toolbar wiring, sheet registration, locale strings, and iso639 localization support.

Changes

Compose Translate Feature

Layer / File(s) Summary
Markdown translation engine
src/providers/translation/translateMarkdown.ts, src/providers/translation/translateMarkdown.test.ts, src/providers/translation/translation.ts
Adds block-splitting, skippable-block detection, request batching/chunking, sequential execution with progress/cancellation support, and exports chunkText; extensive test coverage added.
ISO-639 localized display names
src/utils/iso639.ts, src/utils/iso639.test.ts
languageDisplayName gains an optional uiLang parameter for localized Intl.DisplayNames lookups with fallback handling; new tests validate localized output.
ComposeTranslateModal component and styles
src/components/composeTranslateModal/*
New sheet component detects source language, manages translation state/progress, previews and applies translated markdown with optional title marker; paired stylesheet added.
Sheet registration and barrel exports
src/navigation/sheets.tsx, src/components/index.tsx
Registers SheetNames.COMPOSE_TRANSLATE with payload typing and component mapping; exposes ComposeTranslateModal from the components barrel.
Editor toolbar and view wiring
src/components/markdownEditor/children/editorToolbar.tsx, src/components/markdownEditor/view/markdownEditorView.tsx, src/config/locales/en-US.json
Adds a translate toolbar button and _showTranslateModal handler that opens the sheet and applies returned appendix/title marker to editor state; adds compose_translate locale strings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant EditorToolbar
  participant MarkdownEditorView
  participant ComposeTranslateModal
  participant translateMarkdown

  User->>EditorToolbar: tap translate icon
  EditorToolbar->>MarkdownEditorView: handleShowTranslate()
  MarkdownEditorView->>ComposeTranslateModal: SheetManager.show(COMPOSE_TRANSLATE, {body, title, onApply})
  ComposeTranslateModal->>ComposeTranslateModal: detect source language
  User->>ComposeTranslateModal: select languages, press Translate
  ComposeTranslateModal->>translateMarkdown: translateMarkdown(body, source, target, onProgress)
  translateMarkdown-->>ComposeTranslateModal: translated markdown + progress
  User->>ComposeTranslateModal: press Apply
  ComposeTranslateModal->>MarkdownEditorView: onApply(appendix, titleMarker)
  MarkdownEditorView->>MarkdownEditorView: append appendix to body, update title
Loading

Possibly related PRs

  • ecency/ecency-mobile#3147: Both PRs modify src/components/markdownEditor/children/editorToolbar.tsx to add new toolbar actions that open different sheet modals.
  • ecency/ecency-mobile#3321: Both PRs touch src/utils/iso639.ts's languageDisplayName function around localized language name rendering.

Suggested labels: deploy-pr-build

Poem

A rabbit hops with words anew,
Translating drafts from me to you 🐇
Fences kept, and markers stay,
Progress hops along the way.
Apply the text, the title too—
Hop hop hooray, the feature's true! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature added: compose-side translation in the editor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/compose-translation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a compose-side translation toolbar action that machine-translates the author's own draft using LibreTranslate, appending the result under a divider with a localized language heading. The feature includes franc-based source detection, source/target language pickers, a cancellable progress indicator, RTL-aware preview, and an optional [SRC | TGT] title marker.

  • translateMarkdown.ts: new markdown-aware translator that preserves code fences, images, tables, raw HTML, bare URLs, and horizontal rules, while stripping and re-attaching heading/quote/list markers; consecutive plain paragraphs are batched per request and any failure aborts the whole run.
  • composeTranslateModal.tsx: new COMPOSE_TRANSLATE action-sheet component wired into the editor toolbar via _showTranslateModal; cancellation is managed via a closedRef that the sequential request loop polls at each iteration.

Confidence Score: 5/5

Safe to merge; the translation pipeline is well-isolated, cancellation is correctly gated through closedRef, and all edge cases in the markdown parser are covered by tests.

The core translateMarkdown logic is thoroughly tested (28 tests covering fences, tables, markers, chunking, cancellation, and progress), cancellation propagates correctly through the closedRef pattern, the sheet's state resets cleanly on every open, and the iso639 change is strictly backward compatible. No incorrect data flows or broken contracts were found.

No files require special attention.

Important Files Changed

Filename Overview
src/providers/translation/translateMarkdown.ts New markdown-aware translator; block splitting, skip detection, batch flushing and sequential request loop are all correct; table detection was tightened to require adjacency with a preceding pipe row.
src/components/composeTranslateModal/composeTranslateModal.tsx New translation sheet; state reset on every open, cancellation via closedRef, and the source/target index mapping to LIBRETRANSLATE_CODES are all consistent; translation result and language-pair guard are correct.
src/utils/iso639.ts languageDisplayName gains an optional uiLang arg; existing 1-arg callers see no behavioral change; the improved last-resort fallback (static map before raw code) is strictly better.
src/components/markdownEditor/view/markdownEditorView.tsx _showTranslateModal wires the sheet; body is read from bodyTextRef.current at open time (consistent with AI assist flow); onApply correctly reads the fresh bodyTextRef.current at apply time.
src/providers/translation/translateMarkdown.test.ts 28 tests covering plain paragraphs, batching, fences, images, tables, HTML, horizontal rules, URLs, heading/quote/list markers, nested lists, chunk splitting, emoji, CRLF, progress, rejection, and cancellation.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant EditorToolbar
    participant MarkdownEditorView
    participant SheetManager
    participant ComposeTranslateModal
    participant translateMarkdown
    participant LibreTranslate

    User->>EditorToolbar: tap Translate icon
    EditorToolbar->>MarkdownEditorView: handleShowTranslate()
    MarkdownEditorView->>SheetManager: "show(COMPOSE_TRANSLATE, {body, title, onApply})"
    SheetManager->>ComposeTranslateModal: render with payload

    Note over ComposeTranslateModal: franc detects source lang, useEffect auto-sets target

    User->>ComposeTranslateModal: tap Translate
    ComposeTranslateModal->>translateMarkdown: translateMarkdown(body, src, tgt, onProgress, isCancelled)
    loop For each request batch (sequential)
        translateMarkdown->>LibreTranslate: "POST /translate {q, source, target}"
        LibreTranslate-->>translateMarkdown: "{translatedText}"
        translateMarkdown->>ComposeTranslateModal: onProgress(done, total)
    end
    translateMarkdown-->>ComposeTranslateModal: resolved markdown string
    ComposeTranslateModal->>ComposeTranslateModal: setTranslated(result)

    User->>ComposeTranslateModal: tap Apply
    ComposeTranslateModal->>SheetManager: hide(COMPOSE_TRANSLATE)
    ComposeTranslateModal->>MarkdownEditorView: onApply(appendix, titleMarker)
    MarkdownEditorView->>MarkdownEditorView: _setTextAndSelection(body + appendix)
    MarkdownEditorView->>MarkdownEditorView: onTitleChanged(title + marker)

    alt User swipes to dismiss during translation
        User->>ComposeTranslateModal: swipe down
        ComposeTranslateModal->>ComposeTranslateModal: "closedRef.current = true"
        Note over translateMarkdown: isCancelled() true, throws translate-cancelled
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant EditorToolbar
    participant MarkdownEditorView
    participant SheetManager
    participant ComposeTranslateModal
    participant translateMarkdown
    participant LibreTranslate

    User->>EditorToolbar: tap Translate icon
    EditorToolbar->>MarkdownEditorView: handleShowTranslate()
    MarkdownEditorView->>SheetManager: "show(COMPOSE_TRANSLATE, {body, title, onApply})"
    SheetManager->>ComposeTranslateModal: render with payload

    Note over ComposeTranslateModal: franc detects source lang, useEffect auto-sets target

    User->>ComposeTranslateModal: tap Translate
    ComposeTranslateModal->>translateMarkdown: translateMarkdown(body, src, tgt, onProgress, isCancelled)
    loop For each request batch (sequential)
        translateMarkdown->>LibreTranslate: "POST /translate {q, source, target}"
        LibreTranslate-->>translateMarkdown: "{translatedText}"
        translateMarkdown->>ComposeTranslateModal: onProgress(done, total)
    end
    translateMarkdown-->>ComposeTranslateModal: resolved markdown string
    ComposeTranslateModal->>ComposeTranslateModal: setTranslated(result)

    User->>ComposeTranslateModal: tap Apply
    ComposeTranslateModal->>SheetManager: hide(COMPOSE_TRANSLATE)
    ComposeTranslateModal->>MarkdownEditorView: onApply(appendix, titleMarker)
    MarkdownEditorView->>MarkdownEditorView: _setTextAndSelection(body + appendix)
    MarkdownEditorView->>MarkdownEditorView: onTitleChanged(title + marker)

    alt User swipes to dismiss during translation
        User->>ComposeTranslateModal: swipe down
        ComposeTranslateModal->>ComposeTranslateModal: "closedRef.current = true"
        Note over translateMarkdown: isCancelled() true, throws translate-cancelled
    end
Loading

Reviews (2): Last reviewed commit: "fix(translate): require GFM adjacency fo..." | Re-trigger Greptile

Comment thread src/providers/translation/translateMarkdown.ts
@feruzm feruzm merged commit 4e8f62b into development Jul 6, 2026
10 of 13 checks passed
@feruzm feruzm deleted the feature/compose-translation branch July 6, 2026 17:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/providers/translation/translateMarkdown.ts (1)

140-168: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Heading/quote/list lines never batch, even with each other.

Every translatable marker line becomes its own sequential request (see keeps image and URL lines inside a list untouched test only exercising a single line). For a list-heavy post this means one full round trip per list item, run sequentially — noticeably slower than the plain-paragraph path, which batches up to MAX_BLOCK_BATCH_CHARS.

Consider batching consecutive marker lines of the same block into one request (e.g., joined by a stable, translation-safe separator and split back on response) to cut down round trips for long lists, while keeping the existing per-line behavior as a fallback for edge cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/providers/translation/translateMarkdown.ts` around lines 140 - 168, The
marker-line handling in translateMarkdown currently sends each
heading/quote/list line as its own request, so consecutive translatable lines
never batch together. Update the block processing in translateMarkdown to group
consecutive marker lines from the same block into a single translation request
using a stable separator, then split the translated response back per line when
updating translatedLines and output. Keep the existing per-line request logic as
a fallback for edge cases where batching is unsafe.
src/components/composeTranslateModal/composeTranslateModal.tsx (1)

271-277: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Translated preview bypasses the app's markdown-render pattern.

The translated markdown is shown as raw text instead of through renderPostBody(), unlike the sibling preview elsewhere in the editor (_bodyHtmlForPreview in markdownEditorView.tsx uses renderPostBody(bodyTextRef.current || '...', true, false)). Showing the raw markdown means headings, lists, and quote markers won't visually match what will actually be published, which could confuse the author when validating the translation before applying it.

As per coding guidelines: "Post rendering must use renderPostBody() from @ecency/render-helper for body content display."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/composeTranslateModal/composeTranslateModal.tsx` around lines
271 - 277, The translated preview currently renders raw markdown text instead of
using the app’s standard post renderer. Update the preview block in
composeTranslateModal.tsx so it uses renderPostBody() from `@ecency/render-helper`
for the translated body, matching the existing preview flow in
markdownEditorView.tsx (_bodyHtmlForPreview). Keep the existing
translated/isRtlLang gating and swap the Text-based raw display for the rendered
HTML output so headings, lists, and quotes match published formatting.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/composeTranslateModal/composeTranslateModal.tsx`:
- Around line 105-118: The translation flow in composeTranslateModal is still
committing stale results from an older language pair after the user changes
source or target. Update the _translate logic so it snapshots the requested
source/target pair and only calls setTranslated or setFailed if the current pair
still matches when the request resolves, or otherwise prevent pair changes while
a translation is in flight. Keep the fix aligned with the existing useEffect
reset behavior and the heading/_apply state in ComposeTranslateModal.
- Around line 63-72: The sample generation in composeTranslateModal’s useMemo is
passing 0 into postBodySummary, which causes the preview text to be empty and
breaks source-language detection fallback. Update the sample-building logic in
composeTranslateModal.tsx so postBodySummary receives a non-zero length based on
the sliced body content, and keep the existing SAMPLE_CHARS truncation
afterward.

In `@src/utils/iso639.test.ts`:
- Around line 117-121: The new localization test in languageDisplayName only
covers non-zt inputs, so add a case for the zt + uiLang path to lock down the
duplicate “(Traditional)” behavior. Extend the existing languageDisplayName test
block in iso639.test.ts with an assertion using languageDisplayName('zt', 'zt')
or languageDisplayName('zt', 'es'), and make sure it verifies the corrected
localized name from languageDisplayName in iso639.ts.

In `@src/utils/iso639.ts`:
- Around line 220-237: The language label logic in languageDisplayName is adding
a hardcoded “(Traditional)” suffix for the zt/zh-Hant case, which can duplicate
or mix languages with Intl.DisplayNames output. Update languageDisplayName to
return the Intl-generated name directly when available, and only keep a separate
localized qualifier if needed; reference the languageDisplayName function and
the zt handling branch to locate the change.

---

Nitpick comments:
In `@src/components/composeTranslateModal/composeTranslateModal.tsx`:
- Around line 271-277: The translated preview currently renders raw markdown
text instead of using the app’s standard post renderer. Update the preview block
in composeTranslateModal.tsx so it uses renderPostBody() from
`@ecency/render-helper` for the translated body, matching the existing preview
flow in markdownEditorView.tsx (_bodyHtmlForPreview). Keep the existing
translated/isRtlLang gating and swap the Text-based raw display for the rendered
HTML output so headings, lists, and quotes match published formatting.

In `@src/providers/translation/translateMarkdown.ts`:
- Around line 140-168: The marker-line handling in translateMarkdown currently
sends each heading/quote/list line as its own request, so consecutive
translatable lines never batch together. Update the block processing in
translateMarkdown to group consecutive marker lines from the same block into a
single translation request using a stable separator, then split the translated
response back per line when updating translatedLines and output. Keep the
existing per-line request logic as a fallback for edge cases where batching is
unsafe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 41121548-6600-4264-84fe-cd06d9d814b8

📥 Commits

Reviewing files that changed from the base of the PR and between 0705416 and d505ed5.

📒 Files selected for processing (13)
  • src/components/composeTranslateModal/composeTranslateModal.styles.ts
  • src/components/composeTranslateModal/composeTranslateModal.tsx
  • src/components/composeTranslateModal/index.ts
  • src/components/index.tsx
  • src/components/markdownEditor/children/editorToolbar.tsx
  • src/components/markdownEditor/view/markdownEditorView.tsx
  • src/config/locales/en-US.json
  • src/navigation/sheets.tsx
  • src/providers/translation/translateMarkdown.test.ts
  • src/providers/translation/translateMarkdown.ts
  • src/providers/translation/translation.ts
  • src/utils/iso639.test.ts
  • src/utils/iso639.ts

Comment on lines +63 to +72
const sample = useMemo(
() =>
body
? postBodySummary(body.slice(0, RAW_SAMPLE_CHARS), 0, Platform.OS as any).slice(
0,
SAMPLE_CHARS,
)
: '',
[body],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@ecency/render-helper postBodySummary function accept string or Post object first argument

💡 Result:

The postBodySummary function from @ecency/render-helper accepts a string as its first argument, representing the body of the post [1][2]. While some documentation and usage patterns may refer to passing a post object or "entry" in broader contexts within Ecency applications, the specific implementation of postBodySummary(entryBody, length = 200, platform = "web") expects the first argument to be the post's body content (a string) [2]. When working with full post objects, developers typically pass the body property of that object to this function [3]. Summary of function signature: postBodySummary(entryBody: string, length?: number, platform?: string): string [2] - entryBody: The string content of the post to be summarized [2]. - length: (Optional) The maximum length of the returned summary; defaults to 200 characters [2]. - platform: (Optional) The target platform, which defaults to 'web' [1][2]. Using other values (e.g., 'react-native') adjusts the underlying implementation for mobile environments [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== target file excerpt ==\n'
sed -n '1,140p' src/components/composeTranslateModal/composeTranslateModal.tsx

printf '\n== postBodySummary call sites ==\n'
rg -n "postBodySummary\(" src

Repository: ecency/ecency-mobile

Length of output: 6803


🌐 Web query:

@ecency/render-helper postBodySummary length 0 behavior implementation

💡 Result:

In the @ecency/render-helper library, the postBodySummary function is designed to generate a text summary from post content [1][2]. Its implementation accepts three parameters: entryBody, length, and platform [1][3]. When the length parameter is set to 0, the function's internal logic—specifically where it processes text splitting using a joint function—effectively behaves as an empty or near-empty operation [1][3]. In the source code, the length parameter defaults to 200 [3]. If a value of 0 is passed, the internal text processing (which often involves splitting the body by spaces and truncating to the specified length) results in an empty string or no content being returned, as it enforces a length limit of zero characters [1][3]. Developers typically use this function by importing it from the package and specifying the target platform (e.g., 'web' or 'react-native') to ensure compatibility with different environments [1]. Because it relies on standard markdown-to-text processing, setting the length to 0 is a valid way to request no summary text from the provided post body [1][3].

Citations:


src/components/composeTranslateModal/composeTranslateModal.tsx:66 — pass a non-zero length here. postBodySummary(..., 0, ...) returns an empty string, so sample never contains text and source-language detection falls back to the default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/composeTranslateModal/composeTranslateModal.tsx` around lines
63 - 72, The sample generation in composeTranslateModal’s useMemo is passing 0
into postBodySummary, which causes the preview text to be empty and breaks
source-language detection fallback. Update the sample-building logic in
composeTranslateModal.tsx so postBodySummary receives a non-zero length based on
the sliced body content, and keep the existing SAMPLE_CHARS truncation
afterward.

Comment on lines +105 to +118
useEffect(() => {
if (source !== 'en') {
setTarget('en');
return;
}
const reader = normLang(appLang);
setTarget(reader && reader !== 'en' && LIBRETRANSLATE_TARGETS.has(reader) ? reader : 'es');
}, [source, appLang]);

// A result translated into a previous language pair must never be applied.
useEffect(() => {
setTranslated('');
setFailed(false);
}, [source, target]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/composeTranslateModal/composeTranslateModal.tsx"

echo "== line count =="
wc -l "$FILE"

echo
echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== relevant lines 1 =="
sed -n '1,260p' "$FILE" | nl -ba | sed -n '1,260p' | sed -n '90,230p'

echo
echo "== search translateMarkdown / DropdownButton / translating =="
rg -n "translateMarkdown|DropdownButton|translat|setTranslated|setFailed|source|target|closedRef|_apply" "$FILE"

Repository: ecency/ecency-mobile

Length of output: 593


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/composeTranslateModal/composeTranslateModal.tsx"

echo "== translation handler and state effects =="
sed -n '120,190p' "$FILE" | cat -n

echo
echo "== dropdown JSX =="
sed -n '190,235p' "$FILE" | cat -n

echo
echo "== imports and state declarations =="
sed -n '1,120p' "$FILE" | cat -n

Repository: ecency/ecency-mobile

Length of output: 9607


Stale translation results can overwrite the current language pair.
_translate still commits setTranslated(result) for the old source/target pair after the user changes either dropdown. The reset effect clears the preview, but the in-flight request can repopulate it with a translation that no longer matches the current heading/_apply state. Snapshot the requested pair and only commit if it still matches, or disable the dropdowns while translating.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/composeTranslateModal/composeTranslateModal.tsx` around lines
105 - 118, The translation flow in composeTranslateModal is still committing
stale results from an older language pair after the user changes source or
target. Update the _translate logic so it snapshots the requested source/target
pair and only calls setTranslated or setFailed if the current pair still matches
when the request resolves, or otherwise prevent pair changes while a translation
is in flight. Keep the fix aligned with the existing useEffect reset behavior
and the heading/_apply state in ComposeTranslateModal.

Comment thread src/utils/iso639.test.ts
Comment on lines +117 to +121

it('localizes the name when a UI language is passed', () => {
expect(languageDisplayName('es', 'es').toLowerCase()).toBe('español');
expect(languageDisplayName('de', 'de')).toBe('Deutsch');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add coverage for the zt + uiLang path.

Existing new tests only exercise non-zt codes. Given the duplicate "(Traditional)" suffix bug flagged in iso639.ts, add a case like languageDisplayName('zt', 'zt') or languageDisplayName('zt', 'es') to lock in correct behavior once fixed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/iso639.test.ts` around lines 117 - 121, The new localization test
in languageDisplayName only covers non-zt inputs, so add a case for the zt +
uiLang path to lock down the duplicate “(Traditional)” behavior. Extend the
existing languageDisplayName test block in iso639.test.ts with an assertion
using languageDisplayName('zt', 'zt') or languageDisplayName('zt', 'es'), and
make sure it verifies the corrected localized name from languageDisplayName in
iso639.ts.

Comment thread src/utils/iso639.ts
Comment on lines +220 to +237
export function languageDisplayName(code: string, uiLang?: string): string {
const norm = normLang(code);
if (DISPLAY_NAME_FALLBACK[norm]) {
if (!uiLang && DISPLAY_NAME_FALLBACK[norm]) {
return DISPLAY_NAME_FALLBACK[norm];
}
try {
const AnyIntl = Intl as any;
if (AnyIntl && typeof AnyIntl.DisplayNames === 'function') {
const dn = new AnyIntl.DisplayNames(['en'], { type: 'language' });
const dn = new AnyIntl.DisplayNames(uiLang ? [uiLang, 'en'] : ['en'], { type: 'language' });
const name = dn.of(norm === 'zt' ? 'zh-Hant' : norm);
if (name && name.toLowerCase() !== norm) {
return name;
return norm === 'zt' && uiLang ? `${name} (Traditional)` : name;
}
}
} catch {
// ignore
}
return code;
return DISPLAY_NAME_FALLBACK[norm] ?? code;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file map =="
git ls-files src/utils/iso639.ts src/components/composeTranslateModal.tsx src | sed -n '1,120p'

echo
echo "== iso639 outline =="
ast-grep outline src/utils/iso639.ts --view expanded || true

echo
echo "== search call sites =="
rg -n "languageDisplayName\(" src -S

echo
echo "== relevant slices =="
sed -n '1,220p' src/utils/iso639.ts
echo
sed -n '220,280p' src/utils/iso639.ts
echo
if [ -f src/components/composeTranslateModal.tsx ]; then
  sed -n '1,240p' src/components/composeTranslateModal.tsx
fi

Repository: ecency/ecency-mobile

Length of output: 13316


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('src/utils/iso639.ts')
print(p.exists(), p.stat().st_size if p.exists() else None)
PY

Repository: ecency/ecency-mobile

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== call sites =="
rg -n "languageDisplayName\(" src -S

echo
echo "== composeTranslateModal slice if present =="
fd -a "composeTranslateModal.tsx" src || true

echo
echo "== DisplayNames behavior probe =="
node - <<'JS'
const samples = [
  ['en', 'zh-Hant'],
  ['es', 'zh-Hant'],
  ['zt', 'zh-Hant'],
  ['en', 'zh'],
  ['es', 'zh'],
];
for (const [uiLang, code] of samples) {
  try {
    const dn = new Intl.DisplayNames([uiLang, 'en'], { type: 'language' });
    const name = dn.of(code);
    console.log({ uiLang, code, name });
  } catch (e) {
    console.log({ uiLang, code, error: e.message });
  }
}
JS

Repository: ecency/ecency-mobile

Length of output: 2380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read only the relevant source slices.
if [ -f src/utils/iso639.ts ]; then
  echo "== src/utils/iso639.ts (selected lines) =="
  sed -n '200,280p' src/utils/iso639.ts
fi

echo
echo "== search for languageDisplayName =="
rg -n "languageDisplayName\(" src -S || true

echo
echo "== locate composeTranslateModal.tsx =="
fd -a "composeTranslateModal.tsx" src || true

Repository: ecency/ecency-mobile

Length of output: 3357


Avoid appending (Traditional) in languageDisplayName src/utils/iso639.ts:220-237Intl.DisplayNames already returns a localized zh-Hant name, so the extra English suffix can produce duplicated wording in some locales and mixes languages in others. Return name directly, or localize the qualifier separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/iso639.ts` around lines 220 - 237, The language label logic in
languageDisplayName is adding a hardcoded “(Traditional)” suffix for the
zt/zh-Hant case, which can duplicate or mix languages with Intl.DisplayNames
output. Update languageDisplayName to return the Intl-generated name directly
when available, and only keep a separate localized qualifier if needed;
reference the languageDisplayName function and the zt handling branch to locate
the change.

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