Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [3.5.38] - 2026-06-01

### Fixed — translation rendering & dark-mode polish (from a live debugging pass on anthropic.skilljar.com)
- **No more duplicated text on inline lead-ins** — a paragraph like `<strong>Estimated time:</strong> 15 minutes` rendered as "Estimated time:예상 시간: 15분" (English original + translation side by side). `safeReplaceText` now writes the translation into the first descendant text node and clears every other one (code/pre preserved, inline elements like `<strong>`/`<a>` kept so links stay clickable), so any block with a bold/linked lead-in translates cleanly.
- **Dark-mode floating button is visible again** — the AI Tutor launcher was `#1a1a1a` on a near-black page and effectively disappeared; it now uses a lightened accent with a white icon and a soft ring.
- **"Course roadmap" widget translates** — the embedded `.crm-title` / `.crm-card-h` blocks were not in the translatable selector set (heading and step cards stayed English); they are now covered, and the `.crm` wrapper is re-skinned in dark mode instead of staying bright cream.
- **No uncaught exception after an extension update** — `resume.js` now guards its `chrome.storage.local` calls against an invalidated context ("Extension context invalidated"), which previously surfaced as an uncaught error when a visit was recorded right after a reload/update.
- **Language switch re-localizes the whole sidebar** — switching language after the Tutor sidebar was built left the Tools button, the tools-menu items and the example-question chips frozen at their build-time language; `updateLocalizedLabels` now re-applies all of them.

## [3.5.37] - 2026-06-01

### Fixed — learning-companion robustness (from code review)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<img src="assets/icons/icon128.png" alt="SkillBridge" width="90" />

# SkillBridge — AI Course Translator <!-- VERSION_START -->v3.5.37<!-- VERSION_END -->
# SkillBridge — AI Course Translator <!-- VERSION_START -->v3.5.38<!-- VERSION_END -->

> Available in multiple languages at the [project landing page](https://heznpc.github.io/skillBridge/).

Expand Down
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<title>SkillBridge — AI Course Translator for <!-- LANG_COUNT_START -->32+<!-- LANG_COUNT_END --> Languages</title>
<meta name="description" content="Free Chrome extension to translate the free AI courses at anthropic.skilljar.com into <!-- LANG_COUNT_START -->32+<!-- LANG_COUNT_END --> languages with an in-page AI tutor. No API keys needed.">
<meta property="og:title" content="SkillBridge — AI Course Translator">
<meta property="og:description" content="Translate the free AI courses at anthropic.skilljar.com into your language. <!-- VERSION_START -->v3.5.37<!-- VERSION_END -->.">
<meta property="og:description" content="Translate the free AI courses at anthropic.skilljar.com into your language. <!-- VERSION_START -->v3.5.38<!-- VERSION_END -->.">
<meta property="og:type" content="website">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
Expand Down Expand Up @@ -255,7 +255,7 @@
<body>
<section class="hero">
<div class="container">
<div class="hero-badge"><!-- VERSION_START -->v3.5.37<!-- VERSION_END --> &middot; Open Source &middot; Free &middot; No API Key</div>
<div class="hero-badge"><!-- VERSION_START -->v3.5.38<!-- VERSION_END --> &middot; Open Source &middot; Free &middot; No API Key</div>
<h1>Translate AI Courses<br>Into Your Language</h1>
<p>A Chrome extension that translates the free AI course pages at anthropic.skilljar.com into <!-- LANG_COUNT_START -->32+<!-- LANG_COUNT_END --> languages with an in-page AI tutor.</p>
<p style="font-size:13px;opacity:0.85;margin-top:-16px;">Independent community project. Not affiliated with, endorsed by, or sponsored by Anthropic or Skilljar.</p>
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"description": "__MSG_extDescription__",
"version": "3.5.37",
"version": "3.5.38",
"minimum_chrome_version": "124",
"author": "SkillBridge Contributors",
"homepage_url": "https://github.com/heznpc/skillbridge",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skillbridge",
"version": "3.5.37",
"version": "3.5.38",
"private": true,
"scripts": {
"test": "jest --verbose",
Expand Down
28 changes: 22 additions & 6 deletions src/content/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -1669,15 +1669,31 @@ html.si18n-dark .si18n-history-detail .si18n-chat-bubble code {
background: rgba(255, 255, 255, 0.08) !important;
color: #e0e0e0 !important;
}
/* Floating button in dark mode */
/* Floating button in dark mode.
Was #1a1a1a on a near-black page (≈#0e0e0e) with a muted grey icon — the
launcher visually disappeared, so dark-mode users couldn't find the AI
Tutor entry point. Use a lightened tint of the brand accent with a white
icon and a soft light ring so it clearly reads against the dark canvas. */
html.si18n-dark #skillbridge-fab {
background: #1a1a1a !important;
border-color: rgba(255, 255, 255, 0.1) !important;
color: #b0b0b0 !important;
background: #6b6f9e !important;
border: 1px solid rgba(255, 255, 255, 0.22) !important;
color: #ffffff !important;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55) !important;
}
html.si18n-dark #skillbridge-fab:hover {
background: #252525 !important;
color: #dedede !important;
background: #7c80b3 !important;
color: #ffffff !important;
}

/* Course-author "course roadmap" widget — its `.crm` wrapper ships a fixed
cream background, so in dark mode it rendered as a bright card floating on
the near-black page. Re-skin the wrapper + heading; the step cards
(.crm-c1…c5) carry their own colours and white text, so leave those. */
html.si18n-dark .crm {
background: #1a1a1a !important;
}
html.si18n-dark .crm-title {
color: #ededed !important;
}

/* --- Enrolled Course Page — curriculum, lessons, progress --- */
Expand Down
61 changes: 42 additions & 19 deletions src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@
SKILLJAR_SELECTORS.faqTitle,
`${SKILLJAR_SELECTORS.faqPost} p`,
'div.title',
// Course-author "course roadmap" widget (embedded HTML block). Plain divs
// with text that `div.title` doesn't match — without these the roadmap
// heading and step cards stayed English while the rest of the lesson
// translated.
'.crm-title',
'.crm-card-h',
`${SKILLJAR_SELECTORS.lessonRow} div.title, ${SKILLJAR_SELECTORS.lessonRow} .lesson-wrapper div`,
SKILLJAR_SELECTORS.focusLink,
SKILLJAR_SELECTORS.sectionTitle,
Expand Down Expand Up @@ -708,27 +714,44 @@
return;
}

const textNodes = [];
for (const node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0) {
textNodes.push(node);
}
// `newText` is the translation of the element's ENTIRE visible text,
// including any text that lives inside inline children (<strong>, <a>,
// <em>, …). We must therefore consider every descendant text node — not
// just the element's direct text-node children.
//
// The previous version only collected direct-child text nodes, so a block
// like `<p><strong>Estimated time:</strong> 15 minutes</p>` left the
// <strong> text untranslated and wrote the full translation into the
// trailing text node, rendering "Estimated time:예상 시간: 15분" — the
// English original and its translation duplicated side by side. Any
// paragraph with a bold/linked lead-in hit this.
//
// Writing the whole translation into the FIRST meaningful descendant text
// node and clearing every other one removes the duplication while keeping
// the inline elements in place (links stay clickable, the wrapping
// <strong> survives). Code/pre/script/style text is preserved untouched,
// so inline <code> fragments are never overwritten.
const meaningful = getTextNodes(el);
if (meaningful.length === 0) {
el.textContent = newText;
return;
}

if (textNodes.length === 1) {
textNodes[0].textContent = newText;
} else if (textNodes.length > 1) {
textNodes[0].textContent = newText;
for (let i = 1; i < textNodes.length; i++) textNodes[i].textContent = '';
} else {
const deepTextNodes = getTextNodes(el);
if (deepTextNodes.length > 0) {
deepTextNodes[0].textContent = newText;
for (let i = 1; i < deepTextNodes.length; i++) deepTextNodes[i].textContent = '';
} else {
el.textContent = newText;
}
const target = meaningful[0];

// Blank ALL other descendant text nodes — not just the ones getTextNodes
// returns. getTextNodes skips sub-2-char nodes (punctuation), but those
// can still hold real original text inside a short inline tag (e.g.
// `<b>A</b>`) that would otherwise leak in next to the translation.
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
const toBlank = [];
while (walker.nextNode()) {
const node = walker.currentNode;
if (node === target) continue;
if (node.parentElement?.closest('code, pre, script, style')) continue;
toBlank.push(node);
}
target.textContent = newText;
for (const node of toBlank) node.textContent = '';
}

// Local copy — gt-queue.js has its own (private to the GT pipeline).
Expand Down
50 changes: 46 additions & 4 deletions src/content/resume.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,37 @@
// PERSISTENCE (chrome.storage.local)
// ============================================================

// `chrome.runtime.id` goes undefined the moment the extension context is
// invalidated (dev reload, or an auto-update while this tab stays open).
// After that, touching `chrome.storage.*` throws "Extension context
// invalidated" synchronously. The old code called it unguarded, so a visit
// recorded right after an update surfaced an uncaught exception. Bail
// quietly instead — there is no live extension to persist to anyway.
function extensionAlive() {
try {
return !!chrome.runtime?.id;
} catch {
return false;
}
}

function loadRecent(cb) {
chrome.storage.local.get([STORAGE_KEY], (res) => {
recent = Array.isArray(res[STORAGE_KEY]) ? res[STORAGE_KEY] : [];
if (!extensionAlive()) {
if (cb) cb();
});
return;
}
try {
chrome.storage.local.get([STORAGE_KEY], (res) => {
if (chrome.runtime.lastError) {
if (cb) cb();
return;
}
recent = Array.isArray(res[STORAGE_KEY]) ? res[STORAGE_KEY] : [];
if (cb) cb();
});
} catch {
if (cb) cb();
}
}

let _saveQueue = Promise.resolve();
Expand All @@ -77,7 +103,23 @@
data[STORAGE_KEY] = recent;
_saveQueue = _saveQueue
.catch(() => {})
.then(() => new Promise((resolve) => chrome.storage.local.set(data, resolve)));
.then(
() =>
new Promise((resolve) => {
if (!extensionAlive()) {
resolve();
return;
}
try {
chrome.storage.local.set(data, () => {
void chrome.runtime.lastError; // read to clear, ignore
resolve();
});
} catch {
resolve();
}
}),
);
}

// ============================================================
Expand Down
43 changes: 43 additions & 0 deletions src/content/sidebar-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,49 @@
if (sendBtn) sendBtn.textContent = sb.t(SEND_LABELS);
const askLabel = document.querySelector('.si18n-ask-tutor-label');
if (askLabel) askLabel.textContent = sb.t(ASK_TUTOR_LABELS);

// The sidebar chrome is built once and was previously not re-localized, so
// switching language after the sidebar existed left the tools button, the
// tools-menu items and the example-question chips frozen at their
// build-time language. Re-apply them here.
const toolsBtn = document.getElementById('si18n-tools-btn');
if (toolsBtn) {
const toolsLabel = sb.t(MENU_LABELS.tools);
toolsBtn.title = toolsLabel;
toolsBtn.setAttribute('aria-label', toolsLabel);
}
const closeBtn = document.getElementById('si18n-close');
if (closeBtn) closeBtn.setAttribute('aria-label', sb.t(A11Y_LABELS.closeSidebar));

const menuItems = [
['si18n-recent-btn', sb.t(RESUME_LABELS.openRecent)],
['si18n-bm-btn', sb.t(BOOKMARK_LABELS.openBookmarks)],
['si18n-fc-btn', sb.t(FLASHCARD_LABELS.openFlashcards)],
['si18n-history-btn', sb.t(A11Y_LABELS.chatHistory)],
['si18n-pdf-btn', sb.t(PDF_EXPORT_LABELS.title)],
];
for (const [id, label] of menuItems) {
const span = document.getElementById(id)?.querySelector('span');
if (span) span.textContent = label;
}

// Example-question chips are removed after the first one is clicked, so
// only rebuild while the container is still present. Build via DOM nodes
// (not innerHTML); the click handler is delegated on the container, so
// replacing the children keeps it bound.
const exampleContainer = document.getElementById('si18n-example-questions');
if (exampleContainer) {
const questions = sb.t(EXAMPLE_QUESTIONS) || EXAMPLE_QUESTIONS['en'];
exampleContainer.replaceChildren(
...questions.map((q) => {
const chip = document.createElement('button');
chip.className = 'si18n-example-q';
chip.dataset.question = q;
chip.textContent = q;
return chip;
}),
);
}
}

// ============================================================
Expand Down
2 changes: 1 addition & 1 deletion src/data/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "de",
"langName": "Deutsch",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "es",
"langName": "Español",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "fr",
"langName": "Français",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "it",
"langName": "Italiano",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-05-25",
"translation_provenance": "v1 — derived from src/data/es.json via Spanish→Italian regex transformation (Romance proximity ~80%). Native Italian PRs welcome to replace any section wholesale; structural keys must remain identical to other premium dictionaries (enforced by scripts/check-dict-coverage.js)."
},
Expand Down
2 changes: 1 addition & 1 deletion src/data/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "ja",
"langName": "日本語",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "ko",
"langName": "한국어",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "pt-BR",
"langName": "Português (BR)",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "ru",
"langName": "Русский",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "vi",
"langName": "Tiếng Việt",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "zh-CN",
"langName": "简体中文",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
2 changes: 1 addition & 1 deletion src/data/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"_meta": {
"lang": "zh-TW",
"langName": "中文(繁體)",
"version": "3.5.37",
"version": "3.5.38",
"lastUpdated": "2026-04-02"
},
"ui": {
Expand Down
Loading
Loading