'use strict'; (function attachTabHarborI18n(globalScope) { const LANGUAGE_PREFERENCE_KEY = 'languagePreference'; const LANGUAGE_OPTIONS = new Set(['auto', 'en', 'zh-CN']); const i18nNavigator = globalScope.navigator || {}; const i18nLanguage = String(i18nNavigator.language || i18nNavigator.userLanguage || 'en').toLowerCase(); function resolveAutoLocale() { return i18nLanguage.startsWith('zh') ? 'zh-CN' : 'en'; } function normalizeLanguagePreference(input) { const value = String(input || 'auto'); return LANGUAGE_OPTIONS.has(value) ? value : 'auto'; } function resolveLocaleByPreference(preference) { const normalizedPreference = normalizeLanguagePreference(preference); if (normalizedPreference === 'en') return 'en'; if (normalizedPreference === 'zh-CN') return 'zh-CN'; return resolveAutoLocale(); } let i18nLanguagePreference = 'auto'; let i18nLocale = resolveLocaleByPreference(i18nLanguagePreference); const i18nMessages = { en: { emptyTitle: 'Inbox zero, but for tabs.', emptySubtitle: "You're free.", greetingMorning: 'Good morning', greetingAfternoon: 'Good afternoon', greetingEvening: 'Good evening', timeJustNow: 'just now', timeMinAgo: '{count} min ago', timeHourAgo: '1 hr ago', timeHoursAgo: '{count} hrs ago', timeYesterday: 'yesterday', timeDaysAgo: '{count} days ago', tabsWordSingular: 'tab', tabsWordPlural: 'tabs', groupsWordSingular: 'group', groupsWordPlural: 'groups', openTabsSectionTitle: 'Open tabs', sectionSummary: '{tabs} {tabsWord} across {groups} {groupsWord}', workspacePageHome: 'Home', workspacePageSavedTabs: 'Saved tabs', saveSessionButton: 'Save session', closeAllTabsButton: 'Close all tabs', emptyTabsCount: '0 domains', promptNewGroupName: 'New group name', promptRenameGroupName: 'Rename group', promptTodoTitle: 'Todo title', promptTodoDetails: 'Todo details (optional)', editTodo: 'Edit todo', deleteTodo: 'Delete todo', todoEditorCreateTitle: 'New todo', todoEditorEditTitle: 'Edit todo', todoTitleLabel: 'Title', todoDescriptionLabel: 'Details', todoEditorTitlePlaceholder: 'What needs attention?', todoEditorDescriptionPlaceholder: 'Add notes, context, or a next step.', todoTitleRequired: 'Add a title before saving.', toastTodoUpdated: 'Todo updated', toastTodoDeleted: 'Todo deleted', toastThemeUpdated: 'Theme updated', toastThemeModeUpdated: 'Appearance mode: {mode}', toastBackgroundCleared: 'Background cleared', toastClosedExtraTabHarborTabs: 'Closed extra Tab Harbor tabs', toastMovedTo: 'Moved to {name}', toastCreatedGroup: 'Created {name}', toastRenamedGroup: 'Renamed to {name}', toastCouldNotCreateGroup: 'Could not create group', toastMovedBackToOriginalGroup: 'Moved back to original group', toastPinnedOrder: 'Pinned current order', toastPinOrderOff: 'Pin order turned off', toastTabClosed: 'Tab closed', toastRestoredToOpenTabs: 'Restored to open tabs', toastClosedDuplicatesKeptOne: 'Closed duplicates, kept one copy each', toastAllTabsClosed: 'All tabs closed. Fresh start.', toastQuickTabRemoved: 'Quick tab removed', toastShortcutIconPasted: 'Shortcut icon pasted', toastSvgIconPasted: 'SVG icon pasted', toastClipboardNoImage: 'Clipboard does not contain an image or SVG', toastClipboardUsePasteShortcut: 'Use Cmd/Ctrl+V inside the editor if direct clipboard access is unavailable', toastClipboardTemporaryRef: 'This clipboard image is a temporary file reference. Use Cmd/Ctrl+V instead.', toastShortcutIconUpdated: 'Shortcut icon updated', toastCouldNotUseShortcutImage: 'Could not use shortcut image', toastCouldNotPasteShortcutImage: 'Could not paste shortcut image', toastQuickTabUpdated: 'Quick tab updated', toastQuickTabAdded: 'Quick tab added', toastCouldNotSaveShortcut: 'Could not save shortcut', errorImageCompressionUnavailable: 'Image compression is unavailable', errorPleaseEnterValidUrl: 'Please enter a valid URL', addByUrlTitle: 'Add by URL', shortcutEditTitle: 'Edit shortcut', shortcutAddTitle: 'Add shortcut', shortcutPreviewFallbackLabel: 'Shortcut', shortcutPreviewCustomImageIcon: 'Custom image icon', shortcutPreviewSvgIcon: 'SVG icon', shortcutPreviewEmojiIcon: 'Emoji icon', shortcutPreviewWebsiteIcon: 'Website icon', shortcutPreviewHasCustomIcon: 'Custom icon will replace the site favicon.', shortcutPreviewNoCustomIcon: 'Upload or paste an image, or type an emoji.', closedTabsFromGroup: 'Closed {count} {tabsWord} from {groupLabel}', closedDuplicatesCount: 'Close {count} duplicate{suffix}', duplicatesCount: '{count} duplicate{suffix}', tabsOpenBadge: '{count} {tabsWord} open', tabsLabel: 'tabs', moreCount: '+{count} more', jumpToLabel: 'Jump to {label}', homepagesLabel: 'Homepages', moveToGroup: 'Move to group', renameGroup: 'Rename group', saveButton: 'Save', cancelButton: 'Cancel', noGroupsYet: 'No groups yet', newGroupButton: '+ New group', backToOriginalGroup: 'Back to original group', saveGroupSession: 'Save group session', saveTabSession: 'Save tab session', closeThisTab: 'Close this tab', discardTab: 'Sleep tab', toastTabDiscarded: 'Tab sleeping', toastTabDiscardFailed: 'Failed to sleep tab', toastTabsDiscarded: '{count} tabs sleeping', dragReorderTab: 'Drag to reorder tab', closeGroup: 'Close group', pinnedOrder: 'Pinned order', pinOrder: 'Pin order', deskSettings: 'Desk settings', deskSettingsPanel: 'Desk settings panel', settingsTabAppearance: 'Appearance', settingsTabFeatures: 'Features', appearanceMode: 'Appearance mode', themeModeSystem: 'System', themeModeLight: 'Light', themeModeDark: 'Dark', themeModeFollowingLight: 'Following system: light', themeModeFollowingDark: 'Following system: dark', deskPalette: 'Desk palette', deskBackdrop: 'Desk backdrop', uploadImage: 'Upload image', clearText: 'Clear', surfaceDepth: 'Surface depth', uiScaleLabel: 'Text size', shortcutScaleLabel: 'Shortcut size', hitokotoLabel: 'Hitokoto', editQuickTab: 'Edit quick tab', removeQuickTab: 'Remove quick tab', addQuickTab: 'Add quick tab', addLink: 'Add link', languageLabel: 'Language', languageAuto: 'Auto', languageEnglish: 'English', languageChinese: 'Chinese', chromeTabGroupsLabel: 'Chrome tab groups', toastChromeTabGroupsOn: 'Chrome tab groups on', toastChromeTabGroupsOff: 'Chrome tab groups off', shortcutsLabel: 'Shortcuts', popupShortcutsEmpty: 'No shortcuts yet.', popupTabsEmpty: 'No tabs to display.', ungroupedLabel: 'Ungrouped', closeTabButton: 'Close', popupRefreshLabel: 'Refresh popup content', sessionWordSingular: 'session', sessionWordPlural: 'sessions', restoreSessionButton: 'Restore', savedSessionSettings: 'Session settings', savedSessionSettingsPanel: 'Session settings panel', savedSessionRestoreModeLabel: 'Default open', savedSessionRestoreModeCurrentWindow: 'Current window', savedSessionRestoreModeNewWindow: 'New window', savedSessionNavDisplayModeLabel: 'Top nav display', savedSessionNavDisplayModeIcon: 'Icon', savedSessionNavDisplayModeName: 'Group name', sleepControlLabel: 'Manual sleep control', closeDuplicateNewTabsLabel: 'Auto-close duplicate new tabs', sleepAllTabsButton: 'Sleep all tabs in group', deleteSessionButton: 'Delete session', sessionPickerTitle: 'Choose what to save', sessionPickerClose: 'Close tab picker', sessionPickerModeLabel: 'Save mode', sessionPickerNewSession: 'New group', sessionPickerNewSessionNamePlaceholder: 'Group title (optional)', sessionPickerExistingSession: 'Existing group', sessionPickerTargetSessionLabel: 'Target session', sessionPickerNoSavedSessions: 'No saved sessions yet', sessionPickerAddToExisting: 'Add to session', sessionPickerSaveAndClose: 'Save and close', sessionPickerSelectedCount: '{count} selected', sessionPickerEmpty: 'No restorable tabs in this window.', toastSessionSaved: 'Saved {count} tabs and closed the originals', toastSessionTabsAdded: 'Added {count} tabs to saved session; skipped {skipped} duplicates', toastNoSessionTabsSelected: 'Select at least one tab', toastSessionRestored: 'Restored {count} tabs', toastSavedSessionTabRestored: 'Restored tab', toastSessionDeleted: 'Session deleted', toastSessionActionFailed: 'Could not update saved tabs', }, 'zh-CN': { emptyTitle: '标签页清零了。', emptySubtitle: '你现在很轻松。', greetingMorning: '早上好', greetingAfternoon: '下午好', greetingEvening: '晚上好', timeJustNow: '刚刚', timeMinAgo: '{count} 分钟前', timeHourAgo: '1 小时前', timeHoursAgo: '{count} 小时前', timeYesterday: '昨天', timeDaysAgo: '{count} 天前', tabsWordSingular: '个标签页', tabsWordPlural: '个标签页', groupsWordSingular: '组', groupsWordPlural: '组', openTabsSectionTitle: '打开的标签页', sectionSummary: '{tabs} 个标签页,分布在 {groups} 个分组中', workspacePageHome: '首页', workspacePageSavedTabs: '已保存的标签页', saveSessionButton: '保存会话', closeAllTabsButton: '关闭全部标签页', emptyTabsCount: '0 个域名分组', promptNewGroupName: '新分组名称', promptRenameGroupName: '重命名分组', promptTodoTitle: '待办标题', promptTodoDetails: '待办详情(可选)', editTodo: '编辑待办', deleteTodo: '删除待办', todoEditorCreateTitle: '新建待办', todoEditorEditTitle: '编辑待办', todoTitleLabel: '标题', todoDescriptionLabel: '详情', todoEditorTitlePlaceholder: '接下来要处理什么?', todoEditorDescriptionPlaceholder: '写下补充信息、背景或下一步。', todoTitleRequired: '请先填写待办标题。', toastTodoUpdated: '待办已更新', toastTodoDeleted: '待办已删除', toastThemeUpdated: '主题已更新', toastThemeModeUpdated: '外观模式:{mode}', toastBackgroundCleared: '背景已清除', toastClosedExtraTabHarborTabs: '已关闭多余的 Tab Harbor 标签页', toastMovedTo: '已移动到 {name}', toastCreatedGroup: '已创建 {name}', toastRenamedGroup: '已重命名为 {name}', toastCouldNotCreateGroup: '无法创建分组', toastMovedBackToOriginalGroup: '已移回原始分组', toastPinnedOrder: '已固定当前顺序', toastPinOrderOff: '已取消固定顺序', toastTabClosed: '标签页已关闭', toastRestoredToOpenTabs: '已恢复到打开标签页', toastClosedDuplicatesKeptOne: '已关闭重复标签页,并为每个链接保留一个', toastAllTabsClosed: '已关闭全部标签页,重新开始吧。', toastQuickTabRemoved: '快捷链接已移除', toastShortcutIconPasted: '已粘贴快捷图标', toastSvgIconPasted: '已粘贴 SVG 图标', toastClipboardNoImage: '剪贴板中没有图片或 SVG', toastClipboardUsePasteShortcut: '若无法直接读取剪贴板,请在编辑器中使用 Cmd/Ctrl+V', toastClipboardTemporaryRef: '当前剪贴板图片是临时文件引用,请改用 Cmd/Ctrl+V。', toastShortcutIconUpdated: '快捷图标已更新', toastCouldNotUseShortcutImage: '无法使用该快捷图片', toastCouldNotPasteShortcutImage: '无法粘贴快捷图片', toastQuickTabUpdated: '快捷链接已更新', toastQuickTabAdded: '快捷链接已添加', toastCouldNotSaveShortcut: '无法保存快捷链接', errorImageCompressionUnavailable: '图片压缩不可用', errorPleaseEnterValidUrl: '请输入有效 URL', addByUrlTitle: '通过 URL 添加', shortcutEditTitle: '编辑快捷链接', shortcutAddTitle: '添加快捷链接', shortcutPreviewFallbackLabel: '快捷链接', shortcutPreviewCustomImageIcon: '自定义图片图标', shortcutPreviewSvgIcon: 'SVG 图标', shortcutPreviewEmojiIcon: 'Emoji 图标', shortcutPreviewWebsiteIcon: '网站图标', shortcutPreviewHasCustomIcon: '自定义图标会替换站点 favicon。', shortcutPreviewNoCustomIcon: '上传或粘贴图片,或输入一个 emoji。', closedTabsFromGroup: '已关闭来自 {groupLabel} 的 {count} 个标签页', closedDuplicatesCount: '关闭 {count} 个重复标签页', duplicatesCount: '{count} 个重复标签页', tabsOpenBadge: '已打开 {count} 个标签页', tabsLabel: '标签页', moreCount: '+ 另外 {count} 个', jumpToLabel: '跳转到 {label}', homepagesLabel: '主页', moveToGroup: '移动到分组', renameGroup: '重命名分组', saveButton: '保存', cancelButton: '取消', noGroupsYet: '还没有分组', newGroupButton: '+ 新建分组', backToOriginalGroup: '移回原始分组', saveGroupSession: '保存分组会话', saveTabSession: '保存标签页会话', closeThisTab: '关闭此标签页', discardTab: '休眠标签页', toastTabDiscarded: '标签页已休眠', toastTabDiscardFailed: '休眠失败', toastTabsDiscarded: '已休眠 {count} 个标签页', dragReorderTab: '拖拽重排标签页', closeGroup: '关闭分组', pinnedOrder: '已固定顺序', pinOrder: '固定顺序', deskSettings: '桌面设置', deskSettingsPanel: '桌面设置面板', settingsTabAppearance: '外观', settingsTabFeatures: '功能', appearanceMode: '外观模式', themeModeSystem: '系统', themeModeLight: '浅色', themeModeDark: '深色', themeModeFollowingLight: '当前跟随:浅色', themeModeFollowingDark: '当前跟随:深色', deskPalette: '桌面配色', deskBackdrop: '桌面背景', uploadImage: '上传图片', clearText: '清除', surfaceDepth: '透明度', uiScaleLabel: '字体大小', shortcutScaleLabel: '快捷链接大小', hitokotoLabel: '一言', editQuickTab: '编辑快捷链接', removeQuickTab: '删除快捷链接', addQuickTab: '添加快捷链接', addLink: '添加链接', languageLabel: '语言', languageAuto: '自动', languageEnglish: 'English', languageChinese: '中文', chromeTabGroupsLabel: 'Chrome 标签组', toastChromeTabGroupsOn: '已启用 同步Chrome标签组', toastChromeTabGroupsOff: '已禁用 同步Chrome标签组', shortcutsLabel: '快捷方式', popupShortcutsEmpty: '还没有快捷方式。', popupTabsEmpty: '没有可显示的网页标签页。', ungroupedLabel: '未分组', closeTabButton: '关闭', popupRefreshLabel: '刷新弹窗内容', sessionWordSingular: '个会话', sessionWordPlural: '个会话', restoreSessionButton: '恢复', savedSessionSettings: '会话设置', savedSessionSettingsPanel: '会话设置面板', savedSessionRestoreModeLabel: '默认打开方式', savedSessionRestoreModeCurrentWindow: '此窗口', savedSessionRestoreModeNewWindow: '新窗口', savedSessionNavDisplayModeLabel: '顶部导航显示', savedSessionNavDisplayModeIcon: '图标', savedSessionNavDisplayModeName: '分组名称', sleepControlLabel: '手动休眠控制', closeDuplicateNewTabsLabel: '自动关闭重复新标签页', sleepAllTabsButton: '休眠组内全部标签页', deleteSessionButton: '删除会话', sessionPickerTitle: '选择要保存的内容', sessionPickerClose: '关闭标签页选择器', sessionPickerModeLabel: '保存方式', sessionPickerNewSession: '新建分组', sessionPickerNewSessionNamePlaceholder: '分组标题(可选)', sessionPickerExistingSession: '已有分组', sessionPickerTargetSessionLabel: '目标会话', sessionPickerNoSavedSessions: '还没有已保存会话', sessionPickerAddToExisting: '加入分组', sessionPickerSaveAndClose: '保存并关闭', sessionPickerSelectedCount: '已选择 {count} 个', sessionPickerEmpty: '当前窗口没有可恢复的网页标签页。', toastSessionSaved: '已保存 {count} 个标签页,并关闭原标签页', toastSessionTabsAdded: '已加入 {count} 个标签页到已保存会话;跳过 {skipped} 个重复项', toastNoSessionTabsSelected: '请至少选择一个标签页', toastSessionRestored: '已恢复 {count} 个标签页', toastSavedSessionTabRestored: '标签页已恢复', toastSessionDeleted: '会话已删除', toastSessionActionFailed: '无法更新已保存标签页', }, }; const i18nTextMapZh = { 'Workspace pages': '工作区页面', 'Home': '首页', 'Saved tabs': '已保存的标签页', 'Save current window': '保存当前窗口', 'Choose tabs': '选择标签页', 'Keep a window or a handpicked set of tabs, then bring it back when the desk is ready.': '保存整个窗口或手动挑选的标签页,等工作台准备好时再恢复。', 'Choose what to save': '选择要保存的内容', 'Close tab picker': '关闭标签页选择器', 'Save and close': '保存并关闭', 'Saved sessions': '已保存的会话', 'No saved sessions yet. Save a window when you want the memory back without losing the trail.': '还没有已保存会话。需要释放内存但不想丢失线索时,可以保存当前窗口。', 'Right now': '此刻', 'Search the web': '搜索网络', 'Search with your default engine...': '用默认搜索引擎搜索...', 'Open todos': '打开待办', 'Todos': '待办', 'Close right drawer': '关闭右侧抽屉', 'Archive': '归档', 'Clear archive': '清空归档', 'Search todos': '搜索待办', 'New todo': '新建待办', 'Search todos...': '搜索待办...', 'No tasks yet. Capture the next step when something needs a follow-up.': '还没有任务。需要后续跟进时,把下一步记在这里。', 'Open tabs': '打开的标签页', 'Add open tabs': '现有标签', 'Add by URL': '输入网址', 'Filter tabs...': '筛选标签页...', 'Filter open tabs': '筛选打开的标签页', 'Add selected': '添加选中项', 'Back': '返回', 'Edit shortcut': '编辑快捷链接', 'Close shortcut editor': '关闭快捷链接编辑器', 'Label': '标签', 'Optional shortcut label': '可选的快捷链接标签', 'Shortcut icon preview and paste target': '快捷图标预览与粘贴区域', 'Shortcut icon source': '快捷图标来源', 'Website': '网站', 'Emoji': 'Emoji', 'Image': '图片', 'SVG code': 'SVG 代码', 'Upload image': '上传图片', 'Paste an image with Cmd/Ctrl+V while the editor is focused.': '编辑器聚焦时可用 Cmd/Ctrl+V 粘贴图片。', 'Cancel': '取消', 'Save': '保存', 'Back to top': '返回顶部', 'Desk settings': '桌面设置', 'Desk settings panel': '桌面设置面板', 'Desk palette': '桌面配色', 'Desk backdrop': '桌面背景', 'Clear': '清除', 'Surface depth': '透明度', 'Text size': '字体大小', 'Shortcut size': '快捷链接大小', 'Pinned order': '已固定顺序', 'Pin order': '固定顺序', 'Move to group': '移动到分组', 'No groups yet': '还没有分组', '+ New group': '+ 新建分组', 'Back to original group': '移回原始分组', 'Close this tab': '关闭此标签页', 'Close group': '关闭分组', 'Close extras': '关闭多余标签', 'You have': '你当前有', 'Tab Harbor tabs open. Keep just this one?': '个 Tab Harbor 标签页已打开。仅保留当前这个?', 'Drag to reorder tab': '拖拽重排标签页', 'Drag to reorder todo': '拖拽重排待办', 'Edit todo': '编辑待办', 'Delete todo': '删除待办', 'Delete archived todo': '删除已归档待办', 'Archive': '归档', 'Add link': '添加链接', 'Add quick tab': '添加快捷链接', 'Edit quick tab': '编辑快捷链接', 'Remove quick tab': '删除快捷链接', 'Sync Chrome tab groups': '同步Chrome标签组', 'Homepages': '主页', }; function i18nFormat(template, vars = {}) { return String(template || '').replace(/\{(\w+)\}/g, (_, key) => { return Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : `{${key}}`; }); } function i18nT(key, vars = {}) { const bundle = i18nMessages[i18nLocale] || i18nMessages.en; const fallback = i18nMessages.en || {}; const template = Object.prototype.hasOwnProperty.call(bundle, key) ? bundle[key] : fallback[key]; if (template == null) return key; return i18nFormat(template, vars); } function i18nLocalizeText(input) { const text = String(input == null ? '' : input); if (i18nLocale !== 'zh-CN') return text; if (!text.trim()) return text; const leading = text.match(/^\s*/)?.[0] || ''; const trailing = text.match(/\s*$/)?.[0] || ''; const core = text.trim(); if (Object.prototype.hasOwnProperty.call(i18nTextMapZh, core)) { return `${leading}${i18nTextMapZh[core]}${trailing}`; } let translated = core; translated = translated .replace(/^\+(\d+) more$/i, (_, count) => i18nT('moreCount', { count })) .replace(/^(\d+) tabs? open$/i, (_, count) => i18nT('tabsOpenBadge', { count })) .replace(/^(\d+) duplicate(?:s)?$/i, (_, count) => i18nT('duplicatesCount', { count })) .replace(/^Close (\d+) duplicate(?:s)?$/i, (_, count) => i18nT('closedDuplicatesCount', { count })) .replace(/^Created (.+)$/i, (_, rest) => `创建于 ${rest}`); if (translated !== core) { return `${leading}${translated}${trailing}`; } return text; } function i18nApplyToElement(el) { if (!(el instanceof HTMLElement)) return; if (el.dataset.i18n) { const translated = i18nT(el.dataset.i18n); if (el.textContent !== translated) { el.textContent = translated; } } if (el.dataset.i18nPlaceholder) { const translated = i18nT(el.dataset.i18nPlaceholder); if (el.getAttribute('placeholder') !== translated) { el.setAttribute('placeholder', translated); } } if (el.dataset.i18nAriaLabel) { const translated = i18nT(el.dataset.i18nAriaLabel); if (el.getAttribute('aria-label') !== translated) { el.setAttribute('aria-label', translated); } } if (el.dataset.i18nTitle) { const translated = i18nT(el.dataset.i18nTitle); if (el.getAttribute('title') !== translated) { el.setAttribute('title', translated); } } if (el.dataset.i18nTooltip) { const translated = i18nT(el.dataset.i18nTooltip); if (el.getAttribute('data-tooltip') !== translated) { el.setAttribute('data-tooltip', translated); } } const attrNames = ['aria-label', 'title', 'placeholder', 'data-tooltip']; attrNames.forEach(attr => { const current = el.getAttribute(attr); if (!current) return; const localized = i18nLocalizeText(current); if (localized !== current) { el.setAttribute(attr, localized); } }); } function i18nLocalizeTextNodes(rootNode) { if (i18nLocale !== 'zh-CN') return; const root = rootNode || document.body; if (!root) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; let current = walker.nextNode(); while (current) { nodes.push(current); current = walker.nextNode(); } nodes.forEach(textNode => { const nextValue = i18nLocalizeText(textNode.nodeValue || ''); if (nextValue !== textNode.nodeValue) { textNode.nodeValue = nextValue; } }); } function i18nApplyDomTranslations(rootNode = document) { const root = rootNode && rootNode.querySelectorAll ? rootNode : document; const candidates = root.querySelectorAll ? root.querySelectorAll('[data-i18n],[data-i18n-placeholder],[data-i18n-aria-label],[data-i18n-title],[data-i18n-tooltip], [aria-label], [title], [placeholder], [data-tooltip]') : []; candidates.forEach(i18nApplyToElement); if (root instanceof HTMLElement || root === document || root === document.body) { i18nLocalizeTextNodes(root === document ? document.body : root); } if (document?.documentElement) { document.documentElement.lang = i18nLocale === 'zh-CN' ? 'zh-CN' : 'en'; } } async function loadLanguagePreference() { if (!globalScope.chrome?.storage?.local?.get) { i18nLanguagePreference = 'auto'; i18nLocale = resolveLocaleByPreference(i18nLanguagePreference); return i18nLanguagePreference; } try { const stored = await globalScope.chrome.storage.local.get(LANGUAGE_PREFERENCE_KEY); i18nLanguagePreference = normalizeLanguagePreference(stored[LANGUAGE_PREFERENCE_KEY]); i18nLocale = resolveLocaleByPreference(i18nLanguagePreference); return i18nLanguagePreference; } catch { i18nLanguagePreference = 'auto'; i18nLocale = resolveLocaleByPreference(i18nLanguagePreference); return i18nLanguagePreference; } } async function setLanguagePreference(preference, { persist = true, reload = false } = {}) { i18nLanguagePreference = normalizeLanguagePreference(preference); i18nLocale = resolveLocaleByPreference(i18nLanguagePreference); if (persist && globalScope.chrome?.storage?.local?.set) { await globalScope.chrome.storage.local.set({ [LANGUAGE_PREFERENCE_KEY]: i18nLanguagePreference, }); } if (reload) { globalScope.location?.reload?.(); return i18nLanguagePreference; } i18nApplyDomTranslations(document); return i18nLanguagePreference; } function getLanguagePreference() { return i18nLanguagePreference; } function i18nObserve() { if (!document?.body || i18nLocale !== 'zh-CN') return; let scheduled = false; const scheduleApply = () => { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; i18nApplyDomTranslations(document.body); }); }; const observer = new MutationObserver(scheduleApply); observer.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['aria-label', 'title', 'placeholder', 'data-tooltip'], }); } const waitForDomReady = () => { if (document.readyState !== 'loading') return Promise.resolve(); return new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve, { once: true }); }); }; const i18nReady = (async () => { await loadLanguagePreference(); await waitForDomReady(); i18nApplyDomTranslations(document); i18nObserve(); })(); globalScope.TabHarborI18n = { get locale() { return i18nLocale; }, get isZh() { return i18nLocale === 'zh-CN'; }, t: i18nT, localizeText: i18nLocalizeText, applyDomTranslations: i18nApplyDomTranslations, getLanguagePreference, setLanguagePreference, ready: i18nReady, }; })(typeof globalThis !== 'undefined' ? globalThis : window);