fix(translate): align RTL translated text to the start (#4844)

Inline translation wrappers set lang but never dir, so RTL target
languages (Arabic, Hebrew, Persian, etc.) inherited the source
document's LTR base direction. Justified text then pushed its last
line to the LTR start (left) instead of the RTL start (right).

Derive the wrapper's dir from the target language via
getDirFromLanguage so justified RTL translations align to the start.
Extract the node construction into createTranslationTargetNode to make
the behavior unit-testable.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-29 00:58:56 +08:00
committed by GitHub
parent b87cbfa21a
commit eaf307e71e
2 changed files with 127 additions and 17 deletions
@@ -9,6 +9,44 @@ import { eventDispatcher } from '@/utils/event';
import { walkTextNodes } from '@/utils/walk';
import { debounce } from '@/utils/debounce';
import { getLocale } from '@/utils/misc';
import { getDirFromLanguage } from '@/utils/rtl';
export const createTranslationTargetNode = ({
translatedText,
lang,
targetBlockClassName,
hidden,
widthLineBreak,
}: {
translatedText: string;
lang: string;
targetBlockClassName: string;
hidden: boolean;
widthLineBreak: boolean;
}) => {
const wrapper = document.createElement('font');
wrapper.className = `translation-target ${hidden ? 'hidden' : ''}`;
wrapper.setAttribute('translation-element-mark', '1');
wrapper.setAttribute('lang', lang);
// Set the base direction from the target language so justified RTL text
// (e.g. Arabic) aligns to the start (right) instead of inheriting the
// source document's LTR direction.
wrapper.setAttribute('dir', getDirFromLanguage(lang));
if (widthLineBreak) {
wrapper.appendChild(document.createElement('br'));
}
const blockWrapper = document.createElement('font');
blockWrapper.className = `translation-target ${targetBlockClassName}`;
const inner = document.createElement('font');
inner.className = 'translation-target target-inner target-inner-theme-none';
inner.textContent = translatedText;
blockWrapper.appendChild(inner);
wrapper.appendChild(blockWrapper);
return wrapper;
};
export function useTextTranslation(
bookKey: string,
@@ -260,23 +298,13 @@ export function useTextTranslation(
const translatedText = translated[0];
if (!translatedText || text === translatedText) return;
const wrapper = document.createElement('font');
wrapper.className = `translation-target ${!enabled.current ? 'hidden' : ''}`;
wrapper.setAttribute('translation-element-mark', '1');
wrapper.setAttribute('lang', targetLang || getLocale());
if (widthLineBreak) {
wrapper.appendChild(document.createElement('br'));
}
const blockWrapper = document.createElement('font');
blockWrapper.className = `translation-target ${targetBlockClassName}`;
const inner = document.createElement('font');
inner.className = 'translation-target target-inner target-inner-theme-none';
inner.textContent = translatedText;
blockWrapper.appendChild(inner);
wrapper.appendChild(blockWrapper);
const wrapper = createTranslationTargetNode({
translatedText,
lang: targetLang || getLocale(),
targetBlockClassName,
hidden: !enabled.current,
widthLineBreak,
});
if (el.querySelector('.translation-target')) {
return;