Skip to content

Commit 3960b44

Browse files
Migrate active selection language script
1 parent 01fe936 commit 3960b44

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

injected/src/features/page-context.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ContentFeature from '../content-feature.js';
22
import { getFaviconList } from './favicon.js';
33
import { isDuckAi, isBeingFramed, getTabUrl } from '../utils.js';
4+
import { getActiveSelectionLanguage } from './page-context/utils.js';
45
const MSG_PAGE_CONTEXT_RESPONSE = 'collectionResult';
56

67
function collapseWhitespace(str) {
@@ -76,12 +77,21 @@ export default class PageContext extends ContentFeature {
7677
listenForUrlChanges = true;
7778

7879
init() {
80+
this.setupActiveSelectionLanguageListener();
7981
if (!this.shouldActivate()) {
8082
return;
8183
}
8284
this.setupListeners();
8385
}
8486

87+
setupActiveSelectionLanguageListener() {
88+
this.messaging.subscribe('getActiveSelectionLanguage', () => {
89+
const activeSelectionLanguage = getActiveSelectionLanguage();
90+
this.log.info('Active selection language', activeSelectionLanguage);
91+
this.messaging.notify('getActiveSelectionLanguageResult', { activeSelectionLanguage });
92+
});
93+
}
94+
8595
setupListeners() {
8696
this.observeContentChanges();
8797
if (this.getFeatureSettingEnabled('subscribeToCollect', 'enabled')) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export function getActiveSelectionLanguage() {
2+
// BCP 47 language tag regex
3+
const bcp47 = /^(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE|art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang|(?<langtag>(?<language>[A-Za-z]{2,3}(?<extlang>-[A-Za-z]{3}(-[A-Za-z]{3}){0,2})?|[A-Za-z]{4,8})(?:-(?<script>[A-Za-z]{4}))?(?:-(?<region>[A-Za-z]{2}|[0-9]{3}))?(?<variants>(?:-(?:[0-9A-Za-z]{5,8}|[0-9][0-9A-Za-z]{3}))*)(?<extensions>(?:-(?:[0-9A-WY-Za-wy-z](?:-[0-9A-Za-z]{2,8})+))*)(?:-(?<privateuse>x(?:-[0-9A-Za-z]{1,8})+))?)|(?<privateuse>x(?:-[0-9A-Za-z]{1,8})+))$/mgi
4+
5+
// Get the Selection from the currently focused document (top or same-origin iframe)
6+
function getActiveSelection() {
7+
const activeElement = document.activeElement;
8+
if (activeElement && activeElement.tagName === 'IFRAME') {
9+
try {
10+
return activeElement.contentWindow?.getSelection() || null;
11+
} catch {
12+
// Cross-origin iframe: cannot access its selection
13+
}
14+
}
15+
return window.getSelection?.() || null;
16+
}
17+
18+
const selection = getActiveSelection();
19+
const startContainer = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;
20+
21+
// If no selection, use the document's lang (or null if absent)
22+
if (!startContainer) {
23+
return document.documentElement.getAttribute('lang') || null;
24+
}
25+
26+
// If the start is a text node, step up to its parent element
27+
const startElement =
28+
startContainer.nodeType === Node.ELEMENT_NODE
29+
? startContainer
30+
: startContainer.parentElement;
31+
32+
// Look up the DOM tree for a lang attribute; fallback to the document's lang
33+
const lang = startElement?.closest('[lang]')?.getAttribute('lang') ||
34+
document.documentElement.getAttribute('lang');
35+
36+
// Validate the lang attribute against the BCP 47 regex
37+
if (lang && bcp47.test(lang)) return lang;
38+
39+
return null;
40+
}

0 commit comments

Comments
 (0)