tlrd-extension 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.claude/skills/ostovjs-expert/SKILL.md +72 -0
  3. package/README.md +74 -0
  4. package/icons/icon.svg +4 -0
  5. package/icons/icon128.png +0 -0
  6. package/icons/icon16.png +0 -0
  7. package/icons/icon48.png +0 -0
  8. package/manifest.json +26 -0
  9. package/package.json +25 -0
  10. package/postcss.config.js +7 -0
  11. package/src/app/app.html +13 -0
  12. package/src/app/app.ts +50 -0
  13. package/src/app/models/MessageModel.ts +17 -0
  14. package/src/app/models/MessagesCollection.ts +18 -0
  15. package/src/app/models/PageModel.ts +33 -0
  16. package/src/app/models/SettingsModel.ts +61 -0
  17. package/src/app/services/openai.ts +134 -0
  18. package/src/app/styles/chat.css +38 -0
  19. package/src/app/styles/input.css +13 -0
  20. package/src/app/styles/language-selector.css +18 -0
  21. package/src/app/styles/layout.css +49 -0
  22. package/src/app/styles/loader.css +31 -0
  23. package/src/app/styles/message.css +37 -0
  24. package/src/app/styles/scrollbar.css +17 -0
  25. package/src/app/styles/settings.css +55 -0
  26. package/src/app/styles/tldr.css +49 -0
  27. package/src/app/templates/app-header.hbs +28 -0
  28. package/src/app/templates/chat.hbs +22 -0
  29. package/src/app/templates/language-selector.hbs +16 -0
  30. package/src/app/templates/message.hbs +2 -0
  31. package/src/app/templates/settings.hbs +31 -0
  32. package/src/app/templates/tldr.hbs +29 -0
  33. package/src/app/views/AppView.ts +179 -0
  34. package/src/app/views/ChatView.ts +95 -0
  35. package/src/app/views/LanguageSelectorView.ts +52 -0
  36. package/src/app/views/MessageView.ts +33 -0
  37. package/src/app/views/SettingsView.ts +81 -0
  38. package/src/app/views/TldrView.ts +39 -0
  39. package/src/background/background.ts +14 -0
  40. package/src/content/content.ts +49 -0
  41. package/src/popup/popup.css +166 -0
  42. package/src/popup/popup.html +43 -0
  43. package/src/popup/popup.ts +113 -0
  44. package/src/shared/i18n/index.ts +29 -0
  45. package/src/shared/i18n/locales.ts +36 -0
  46. package/src/shared/i18n/translations/ar.ts +36 -0
  47. package/src/shared/i18n/translations/de.ts +36 -0
  48. package/src/shared/i18n/translations/en.ts +36 -0
  49. package/src/shared/i18n/translations/es.ts +36 -0
  50. package/src/shared/i18n/translations/fr.ts +36 -0
  51. package/src/shared/i18n/translations/hi.ts +36 -0
  52. package/src/shared/i18n/translations/ja.ts +36 -0
  53. package/src/shared/i18n/translations/pt.ts +36 -0
  54. package/src/shared/i18n/translations/ru.ts +36 -0
  55. package/src/shared/i18n/translations/zh.ts +36 -0
  56. package/src/types/handlebars.d.ts +4 -0
  57. package/tailwind.config.js +39 -0
  58. package/tsconfig.json +18 -0
  59. package/vite.config.ts +67 -0
@@ -0,0 +1,52 @@
1
+ import { View } from 'ostovjs';
2
+ import template from '../templates/language-selector.hbs';
3
+ import { SUMMARY_LANGUAGES, UI_LANGUAGES } from '../../shared/i18n/locales';
4
+ import type SettingsModel from '../models/SettingsModel';
5
+
6
+ class LanguageSelectorView extends View {
7
+ declare model: SettingsModel;
8
+
9
+ constructor(options: { model: SettingsModel }) {
10
+ super({
11
+ ...options,
12
+ tagName: 'div',
13
+ className: 'lang-selector',
14
+ events: {
15
+ 'change [data-action=change-summary-lang]': 'onChangeSummaryLang',
16
+ 'change [data-action=change-ui-lang]': 'onChangeUiLang',
17
+ },
18
+ });
19
+ }
20
+
21
+ render() {
22
+ const summaryLang = this.model.get('summaryLanguage');
23
+ const uiLang = this.model.get('uiLanguage');
24
+
25
+ (this.el as HTMLElement).innerHTML = template({
26
+ summaryLanguages: SUMMARY_LANGUAGES.map((l) => ({
27
+ ...l,
28
+ selected: l.code === summaryLang,
29
+ })),
30
+ uiLanguages: UI_LANGUAGES.map((l) => ({
31
+ ...l,
32
+ selected: l.code === uiLang,
33
+ })),
34
+ });
35
+ return this;
36
+ }
37
+
38
+ onChangeSummaryLang(e: Event) {
39
+ const value = (e.target as HTMLSelectElement).value;
40
+ this.model.set('summaryLanguage', value);
41
+ this.model.persist();
42
+ }
43
+
44
+ onChangeUiLang(e: Event) {
45
+ const value = (e.target as HTMLSelectElement).value;
46
+ this.model.set('uiLanguage', value);
47
+ this.model.persist();
48
+ this.trigger('locale-changed', value);
49
+ }
50
+ }
51
+
52
+ export default LanguageSelectorView;
@@ -0,0 +1,33 @@
1
+ import { View } from 'ostovjs';
2
+ import template from '../templates/message.hbs';
3
+ import { t } from '../../shared/i18n';
4
+ import type MessageModel from '../models/MessageModel';
5
+
6
+ class MessageView extends View {
7
+ declare model: MessageModel;
8
+
9
+ constructor(options: { model: MessageModel }) {
10
+ const role = options.model.get('role');
11
+ super({
12
+ ...options,
13
+ tagName: 'div',
14
+ className: `message message--${role}`,
15
+ });
16
+ }
17
+
18
+ render() {
19
+ const role = this.model.get('role');
20
+ (this.el as HTMLElement).innerHTML = template({
21
+ roleLabel: role === 'user' ? t('app.chat.role.user') : t('app.chat.role.assistant'),
22
+ content: this.model.get('content'),
23
+ });
24
+ return this;
25
+ }
26
+
27
+ updateContent(text: string) {
28
+ const bubble = (this.el as HTMLElement).querySelector('.message__bubble');
29
+ if (bubble) bubble.textContent = text;
30
+ }
31
+ }
32
+
33
+ export default MessageView;
@@ -0,0 +1,81 @@
1
+ import { View } from 'ostovjs';
2
+ import template from '../templates/settings.hbs';
3
+ import { t } from '../../shared/i18n';
4
+ import { AVAILABLE_MODELS } from '../models/SettingsModel';
5
+ import type SettingsModel from '../models/SettingsModel';
6
+
7
+ class SettingsView extends View {
8
+ declare model: SettingsModel;
9
+ private _status = '';
10
+ private _statusType = '';
11
+
12
+ constructor(options: { model: SettingsModel }) {
13
+ super({
14
+ ...options,
15
+ tagName: 'div',
16
+ className: 'settings',
17
+ events: {
18
+ 'click [data-action=save]': 'onSave',
19
+ 'keydown #api-key': 'onKeydown',
20
+ 'change [data-action=change-model]': 'onChangeModel',
21
+ },
22
+ });
23
+ }
24
+
25
+ initialize() {
26
+ this.listenTo(this.model, 'saved', this.onSaved);
27
+ this.listenTo(this.model, 'loaded', this.render);
28
+ }
29
+
30
+ render() {
31
+ const currentModel = this.model.get('model');
32
+ (this.el as HTMLElement).innerHTML = template({
33
+ apiKey: this.model.get('apiKey'),
34
+ status: this._status,
35
+ statusType: this._statusType,
36
+ models: AVAILABLE_MODELS.map((m) => ({
37
+ ...m,
38
+ selected: m.id === currentModel,
39
+ })),
40
+ });
41
+ return this;
42
+ }
43
+
44
+ onKeydown(e: KeyboardEvent) {
45
+ if (e.key === 'Enter') this.onSave();
46
+ }
47
+
48
+ onSave() {
49
+ const input = (this.el as HTMLElement).querySelector<HTMLInputElement>('#api-key');
50
+ const key = input?.value.trim();
51
+
52
+ if (!key) {
53
+ this._status = t('app.settings.required');
54
+ this._statusType = 'error';
55
+ this.render();
56
+ return;
57
+ }
58
+
59
+ this.model.set('apiKey', key);
60
+ this.model.persist();
61
+ }
62
+
63
+ onChangeModel(e: Event) {
64
+ const value = (e.target as HTMLSelectElement).value;
65
+ this.model.set('model', value);
66
+ this.model.persist();
67
+ }
68
+
69
+ onSaved() {
70
+ this._status = t('app.settings.saved');
71
+ this._statusType = 'saved';
72
+ this.render();
73
+
74
+ setTimeout(() => {
75
+ this._status = '';
76
+ this.render();
77
+ }, 2000);
78
+ }
79
+ }
80
+
81
+ export default SettingsView;
@@ -0,0 +1,39 @@
1
+ import { View } from 'ostovjs';
2
+ import template from '../templates/tldr.hbs';
3
+ import type PageModel from '../models/PageModel';
4
+
5
+ class TldrView extends View {
6
+ declare model: PageModel;
7
+
8
+ constructor(options: { model: PageModel }) {
9
+ super({
10
+ ...options,
11
+ tagName: 'div',
12
+ className: 'tldr',
13
+ });
14
+ }
15
+
16
+ initialize() {
17
+ this.listenTo(this.model, 'change', this.render);
18
+ }
19
+
20
+ render() {
21
+ (this.el as HTMLElement).innerHTML = template({
22
+ pageTitle: this.model.get('title'),
23
+ pageUrl: this.model.get('url'),
24
+ tldr: this.model.get('tldr'),
25
+ loading: this.model.get('loading'),
26
+ error: this.model.get('error'),
27
+ });
28
+ return this;
29
+ }
30
+
31
+ updateContent(text: string) {
32
+ const contentEl = (this.el as HTMLElement).querySelector('.tldr__content');
33
+ if (contentEl) {
34
+ contentEl.textContent = text;
35
+ }
36
+ }
37
+ }
38
+
39
+ export default TldrView;
@@ -0,0 +1,14 @@
1
+ chrome.runtime.onMessage.addListener((message) => {
2
+ if (message.type === 'OPEN_TLDR') {
3
+ chrome.storage.session.set({ pendingPageContent: message.content }).then(() => {
4
+ const params = new URLSearchParams({
5
+ title: message.title || '',
6
+ url: message.url || '',
7
+ });
8
+
9
+ chrome.tabs.create({
10
+ url: chrome.runtime.getURL(`src/app/app.html?${params.toString()}`),
11
+ });
12
+ });
13
+ }
14
+ });
@@ -0,0 +1,49 @@
1
+ interface PageData {
2
+ title: string;
3
+ url: string;
4
+ content: string;
5
+ }
6
+
7
+ function extractPageContent(): string {
8
+ // Use the broadest semantic container, not just the first article
9
+ const containerSelectors = [
10
+ '[role="main"]',
11
+ 'main',
12
+ '#content',
13
+ '.content',
14
+ ];
15
+
16
+ let contentEl: Element | null = null;
17
+
18
+ for (const selector of containerSelectors) {
19
+ contentEl = document.querySelector(selector);
20
+ if (contentEl) break;
21
+ }
22
+
23
+ if (!contentEl) {
24
+ contentEl = document.body;
25
+ }
26
+
27
+ const clone = contentEl.cloneNode(true) as HTMLElement;
28
+
29
+ const removable = clone.querySelectorAll(
30
+ 'script, style, noscript, svg, nav, header, footer, aside, .sidebar, .nav, .menu, .ad, .advertisement, [role="navigation"], [role="banner"], [role="complementary"], [hidden]'
31
+ );
32
+ removable.forEach((el) => el.remove());
33
+
34
+ return (clone.textContent || '').replace(/\s+/g, ' ').trim();
35
+ }
36
+
37
+ function getPageData(): PageData {
38
+ return {
39
+ title: document.title,
40
+ url: window.location.href,
41
+ content: extractPageContent(),
42
+ };
43
+ }
44
+
45
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
46
+ if (message.type === 'EXTRACT_CONTENT') {
47
+ sendResponse(getPageData());
48
+ }
49
+ });
@@ -0,0 +1,166 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
9
+ width: 300px;
10
+ background: #f8f9fa;
11
+ }
12
+
13
+ .popup__header {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 8px;
17
+ padding: 14px 16px;
18
+ background: #fff;
19
+ border-bottom: 1px solid #e9ecef;
20
+ }
21
+
22
+ .popup__logo-icon {
23
+ width: 22px;
24
+ height: 22px;
25
+ color: #5c7cfa;
26
+ }
27
+
28
+ .popup__title {
29
+ font-size: 16px;
30
+ font-weight: 700;
31
+ color: #4263eb;
32
+ letter-spacing: -0.02em;
33
+ flex: 1;
34
+ }
35
+
36
+ .popup__settings-toggle {
37
+ background: none;
38
+ border: none;
39
+ padding: 4px;
40
+ border-radius: 6px;
41
+ color: #868e96;
42
+ cursor: pointer;
43
+ transition: color 0.15s;
44
+ }
45
+
46
+ .popup__settings-toggle:hover {
47
+ color: #495057;
48
+ }
49
+
50
+ .popup__body {
51
+ padding: 16px;
52
+ }
53
+
54
+ .popup__section {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 10px;
58
+ }
59
+
60
+ .popup__desc {
61
+ font-size: 13px;
62
+ color: #868e96;
63
+ }
64
+
65
+ .popup__label {
66
+ font-size: 13px;
67
+ font-weight: 500;
68
+ color: #495057;
69
+ }
70
+
71
+ .popup__input-wrap {
72
+ display: flex;
73
+ gap: 8px;
74
+ }
75
+
76
+ .popup__input {
77
+ flex: 1;
78
+ padding: 8px 12px;
79
+ border: 1px solid #dee2e6;
80
+ border-radius: 10px;
81
+ font-size: 13px;
82
+ font-family: inherit;
83
+ background: #f8f9fa;
84
+ color: #212529;
85
+ outline: none;
86
+ transition: border-color 0.15s;
87
+ }
88
+
89
+ .popup__input:focus {
90
+ border-color: #748ffc;
91
+ }
92
+
93
+ .popup__save-btn {
94
+ padding: 8px 14px;
95
+ border: none;
96
+ border-radius: 10px;
97
+ background: #4c6ef5;
98
+ color: #fff;
99
+ font-family: inherit;
100
+ font-size: 13px;
101
+ font-weight: 500;
102
+ cursor: pointer;
103
+ transition: background 0.15s;
104
+ }
105
+
106
+ .popup__save-btn:hover {
107
+ background: #4263eb;
108
+ }
109
+
110
+ .popup__link {
111
+ font-size: 12px;
112
+ color: #4c6ef5;
113
+ text-decoration: none;
114
+ }
115
+
116
+ .popup__link:hover {
117
+ text-decoration: underline;
118
+ }
119
+
120
+
121
+ .popup__btn {
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ gap: 8px;
126
+ width: 100%;
127
+ padding: 10px 16px;
128
+ border: none;
129
+ border-radius: 12px;
130
+ background: #4c6ef5;
131
+ color: #fff;
132
+ font-family: inherit;
133
+ font-size: 14px;
134
+ font-weight: 500;
135
+ cursor: pointer;
136
+ transition: background 0.2s;
137
+ }
138
+
139
+ .popup__btn:hover {
140
+ background: #4263eb;
141
+ }
142
+
143
+ .popup__btn:active {
144
+ background: #3b5bdb;
145
+ }
146
+
147
+ .popup__btn:disabled {
148
+ background: #ced4da;
149
+ color: #868e96;
150
+ cursor: not-allowed;
151
+ }
152
+
153
+ .popup__status {
154
+ font-size: 12px;
155
+ color: #868e96;
156
+ text-align: center;
157
+ min-height: 16px;
158
+ }
159
+
160
+ .popup__status--error {
161
+ color: #e03131;
162
+ }
163
+
164
+ .popup__status--saved {
165
+ color: #2f9e44;
166
+ }
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
7
+ </head>
8
+ <body>
9
+ <div class="popup">
10
+ <div class="popup__header">
11
+ <svg class="popup__logo-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
13
+ </svg>
14
+ <span class="popup__title">TLDR</span>
15
+ <button class="popup__settings-toggle" id="toggle-settings-btn">
16
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:16px;height:16px;">
17
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
19
+ </svg>
20
+ </button>
21
+ </div>
22
+ <div class="popup__body">
23
+ <!-- Settings section -->
24
+ <div class="popup__section" id="settings-section" style="display:none">
25
+ <label class="popup__label" data-i18n="popup.apiKey.label"></label>
26
+ <div class="popup__input-wrap">
27
+ <input class="popup__input" id="api-key-input" type="password" placeholder="sk-..." />
28
+ <button class="popup__save-btn" id="save-key-btn" data-i18n="popup.apiKey.save"></button>
29
+ </div>
30
+ <a class="popup__link" href="https://platform.openai.com/api-keys" target="_blank" rel="noopener" data-i18n="popup.apiKey.link"></a>
31
+ <p class="popup__status" id="key-status"></p>
32
+ </div>
33
+ <!-- Ready section -->
34
+ <div class="popup__section" id="ready-section" style="display:none">
35
+ <p class="popup__desc" data-i18n="popup.desc"></p>
36
+ <button class="popup__btn" id="summarize-btn" data-i18n="popup.btn.summarize"></button>
37
+ <p class="popup__status" id="status"></p>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <script type="module" src="popup.ts"></script>
42
+ </body>
43
+ </html>
@@ -0,0 +1,113 @@
1
+ import './popup.css';
2
+ import { t, setLocale } from '../shared/i18n';
3
+ import type { SupportedLocale } from '../shared/i18n/locales';
4
+
5
+ const settingsSection = document.getElementById('settings-section')!;
6
+ const readySection = document.getElementById('ready-section')!;
7
+ const apiKeyInput = document.getElementById('api-key-input') as HTMLInputElement;
8
+ const saveKeyBtn = document.getElementById('save-key-btn')!;
9
+ const keyStatus = document.getElementById('key-status')!;
10
+ const summarizeBtn = document.getElementById('summarize-btn') as HTMLButtonElement;
11
+ const status = document.getElementById('status')!;
12
+ const toggleSettingsBtn = document.getElementById('toggle-settings-btn')!;
13
+
14
+ function translateUI() {
15
+ document.querySelectorAll<HTMLElement>('[data-i18n]').forEach((el) => {
16
+ el.textContent = t(el.dataset.i18n!);
17
+ });
18
+ }
19
+
20
+ function showSection(hasKey: boolean) {
21
+ settingsSection.style.display = hasKey ? 'none' : '';
22
+ readySection.style.display = hasKey ? '' : 'none';
23
+ }
24
+
25
+ async function init() {
26
+ const data = await chrome.storage.local.get(['apiKey', 'uiLanguage']);
27
+ const uiLang = (data.uiLanguage || 'en') as SupportedLocale;
28
+ setLocale(uiLang);
29
+ translateUI();
30
+ showSection(Boolean(data.apiKey));
31
+ }
32
+
33
+ saveKeyBtn.addEventListener('click', async () => {
34
+ const key = apiKeyInput.value.trim();
35
+ if (!key) {
36
+ keyStatus.textContent = t('popup.apiKey.required');
37
+ keyStatus.className = 'popup__status popup__status--error';
38
+ return;
39
+ }
40
+
41
+ await chrome.storage.local.set({ apiKey: key });
42
+ keyStatus.textContent = t('popup.apiKey.saved');
43
+ keyStatus.className = 'popup__status popup__status--saved';
44
+ setTimeout(() => showSection(true), 500);
45
+ });
46
+
47
+ apiKeyInput.addEventListener('keydown', (e) => {
48
+ if (e.key === 'Enter') saveKeyBtn.click();
49
+ });
50
+
51
+ toggleSettingsBtn.addEventListener('click', () => {
52
+ const isVisible = settingsSection.style.display !== 'none';
53
+ settingsSection.style.display = isVisible ? 'none' : '';
54
+ });
55
+
56
+ async function extractContent(tabId: number): Promise<PageData> {
57
+ // Inject content script
58
+ await chrome.scripting.executeScript({
59
+ target: { tabId },
60
+ files: ['content.js'],
61
+ });
62
+
63
+ // Retry sending message — script may not have registered listener yet
64
+ for (let attempt = 0; attempt < 3; attempt++) {
65
+ try {
66
+ const response = await chrome.tabs.sendMessage(tabId, { type: 'EXTRACT_CONTENT' });
67
+ if (response?.content) return response as PageData;
68
+ } catch {
69
+ // listener not ready yet
70
+ }
71
+ await new Promise((r) => setTimeout(r, 100));
72
+ }
73
+
74
+ throw new Error(t('error.noContent'));
75
+ }
76
+
77
+ interface PageData {
78
+ title: string;
79
+ url: string;
80
+ content: string;
81
+ }
82
+
83
+ summarizeBtn.addEventListener('click', async () => {
84
+ summarizeBtn.disabled = true;
85
+ status.textContent = t('popup.status.extracting');
86
+ status.className = 'popup__status';
87
+
88
+ try {
89
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
90
+
91
+ if (!tab?.id) {
92
+ throw new Error(t('error.noTab'));
93
+ }
94
+
95
+ const response = await extractContent(tab.id);
96
+
97
+ chrome.runtime.sendMessage({
98
+ type: 'OPEN_TLDR',
99
+ title: response.title,
100
+ url: response.url,
101
+ content: response.content,
102
+ });
103
+
104
+ window.close();
105
+ } catch (err) {
106
+ const message = err instanceof Error ? err.message : 'Unknown error';
107
+ status.textContent = message;
108
+ status.className = 'popup__status popup__status--error';
109
+ summarizeBtn.disabled = false;
110
+ }
111
+ });
112
+
113
+ init();
@@ -0,0 +1,29 @@
1
+ import en from './translations/en';
2
+ import zh from './translations/zh';
3
+ import es from './translations/es';
4
+ import hi from './translations/hi';
5
+ import ar from './translations/ar';
6
+ import fr from './translations/fr';
7
+ import pt from './translations/pt';
8
+ import ru from './translations/ru';
9
+ import ja from './translations/ja';
10
+ import de from './translations/de';
11
+ import type { SupportedLocale } from './locales';
12
+
13
+ const translations: Record<SupportedLocale, Record<string, string>> = {
14
+ en, zh, es, hi, ar, fr, pt, ru, ja, de,
15
+ };
16
+
17
+ let currentLocale: SupportedLocale = 'en';
18
+
19
+ export function setLocale(locale: SupportedLocale): void {
20
+ currentLocale = locale;
21
+ }
22
+
23
+ export function getLocale(): SupportedLocale {
24
+ return currentLocale;
25
+ }
26
+
27
+ export function t(key: string): string {
28
+ return translations[currentLocale]?.[key] ?? translations['en'][key] ?? key;
29
+ }
@@ -0,0 +1,36 @@
1
+ export type SupportedLocale = 'en' | 'zh' | 'es' | 'hi' | 'ar' | 'fr' | 'pt' | 'ru' | 'ja' | 'de';
2
+ export type SummaryLanguage = SupportedLocale | 'original';
3
+
4
+ export interface LanguageOption {
5
+ code: string;
6
+ nativeLabel: string;
7
+ }
8
+
9
+ export const SUMMARY_LANGUAGES: LanguageOption[] = [
10
+ { code: 'original', nativeLabel: 'Original' },
11
+ { code: 'en', nativeLabel: 'English' },
12
+ { code: 'zh', nativeLabel: '中文' },
13
+ { code: 'es', nativeLabel: 'Español' },
14
+ { code: 'hi', nativeLabel: 'हिन्दी' },
15
+ { code: 'ar', nativeLabel: 'العربية' },
16
+ { code: 'fr', nativeLabel: 'Français' },
17
+ { code: 'pt', nativeLabel: 'Português' },
18
+ { code: 'ru', nativeLabel: 'Русский' },
19
+ { code: 'ja', nativeLabel: '日本語' },
20
+ { code: 'de', nativeLabel: 'Deutsch' },
21
+ ];
22
+
23
+ export const UI_LANGUAGES: LanguageOption[] = SUMMARY_LANGUAGES.filter((l) => l.code !== 'original');
24
+
25
+ export const LANGUAGE_NAMES: Record<string, string> = {
26
+ en: 'English',
27
+ zh: 'Chinese',
28
+ es: 'Spanish',
29
+ hi: 'Hindi',
30
+ ar: 'Arabic',
31
+ fr: 'French',
32
+ pt: 'Portuguese',
33
+ ru: 'Russian',
34
+ ja: 'Japanese',
35
+ de: 'German',
36
+ };