autumnnote 1.0.7 → 1.0.9

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 (40) hide show
  1. package/README.md +332 -638
  2. package/dist/autumnnote.css +40 -4
  3. package/dist/autumnnote.es.js +3375 -262
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +3374 -261
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/js/Context.js +4 -0
  9. package/src/js/editing/Style.js +195 -44
  10. package/src/js/editing/Typing.js +2 -2
  11. package/src/js/i18n/de.js +320 -0
  12. package/src/js/i18n/en.js +328 -0
  13. package/src/js/i18n/es.js +320 -0
  14. package/src/js/i18n/fr.js +321 -0
  15. package/src/js/i18n/index.js +59 -0
  16. package/src/js/i18n/ja.js +321 -0
  17. package/src/js/i18n/ko.js +320 -0
  18. package/src/js/i18n/vi.js +321 -0
  19. package/src/js/i18n/zh.js +321 -0
  20. package/src/js/index.js +2 -1
  21. package/src/js/module/CodeTooltip.js +12 -9
  22. package/src/js/module/ContextMenu.js +13 -11
  23. package/src/js/module/Editor.js +14 -3
  24. package/src/js/module/EmojiDialog.js +19 -7
  25. package/src/js/module/FindReplace.js +14 -13
  26. package/src/js/module/IconDialog.js +25 -13
  27. package/src/js/module/ImageDialog.js +25 -20
  28. package/src/js/module/ImageTooltip.js +13 -12
  29. package/src/js/module/LinkDialog.js +10 -9
  30. package/src/js/module/LinkTooltip.js +6 -5
  31. package/src/js/module/Placeholder.js +6 -1
  32. package/src/js/module/ShortcutsDialog.js +5 -4
  33. package/src/js/module/Statusbar.js +6 -5
  34. package/src/js/module/TableTooltip.js +412 -102
  35. package/src/js/module/Toolbar.js +21 -15
  36. package/src/js/module/VideoDialog.js +10 -9
  37. package/src/js/module/VideoTooltip.js +12 -11
  38. package/src/js/settings.js +4 -0
  39. package/src/styles/autumnnote.scss +50 -5
  40. package/types/index.d.ts +58 -1
@@ -122,7 +122,7 @@ export class FindReplace {
122
122
  const isReplace = this._mode === 'replace';
123
123
  if (replaceRow) replaceRow.style.display = isReplace ? '' : 'none';
124
124
  if (replaceActions) replaceActions.style.display = isReplace ? '' : 'none';
125
- if (title) title.textContent = isReplace ? 'Find & Replace' : 'Find';
125
+ if (title) title.textContent = isReplace ? this.context.locale.findReplace.findReplaceTitle : this.context.locale.findReplace.findTitle;
126
126
  }
127
127
 
128
128
  // ---------------------------------------------------------------------------
@@ -130,24 +130,25 @@ export class FindReplace {
130
130
  // ---------------------------------------------------------------------------
131
131
 
132
132
  _buildDialog() {
133
+ const L = this.context.locale.findReplace;
133
134
  const overlay = createElement('div', {
134
135
  class: 'an-dialog-overlay an-fr-dialog',
135
136
  role: 'dialog',
136
137
  'aria-modal': 'true',
137
- 'aria-label': 'Find and Replace',
138
+ 'aria-label': L.findReplaceTitle,
138
139
  });
139
140
  const box = createElement('div', { class: 'an-dialog-box' });
140
141
 
141
142
  // ---- Title row ----
142
143
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
143
144
  const title = createElement('h3', { class: 'an-dialog-title' });
144
- title.textContent = 'Find';
145
+ title.textContent = L.findTitle;
145
146
  const closeBtn = createElement('button', {
146
147
  type: 'button',
147
148
  class: 'an-icon-close',
148
149
  'aria-label': 'Close',
149
150
  });
150
- closeBtn.textContent = '×';
151
+ closeBtn.textContent = L.close;
151
152
  this._closeBtn = closeBtn;
152
153
  titleRow.append(title, closeBtn);
153
154
  box.appendChild(titleRow);
@@ -157,8 +158,8 @@ export class FindReplace {
157
158
  const findInput = createElement('input', {
158
159
  type: 'text',
159
160
  class: 'an-input',
160
- placeholder: 'Find…',
161
- 'aria-label': 'Search text',
161
+ placeholder: L.findPlaceholder,
162
+ 'aria-label': L.searchAriaLabel,
162
163
  });
163
164
  this._findInput = findInput;
164
165
  findRow.appendChild(findInput);
@@ -172,7 +173,7 @@ export class FindReplace {
172
173
  'aria-label': 'Case sensitive',
173
174
  });
174
175
  this._caseCheckbox = caseCheckbox;
175
- caseLabel.append(caseCheckbox, document.createTextNode('\u00a0Case sensitive'));
176
+ caseLabel.append(caseCheckbox, document.createTextNode(L.caseSensitive));
176
177
  const counter = createElement('span', { class: 'an-fr-counter' });
177
178
  this._counterEl = counter;
178
179
  optRow.append(caseLabel, counter);
@@ -181,9 +182,9 @@ export class FindReplace {
181
182
  // ---- Find actions ----
182
183
  const findActions = createElement('div', { class: 'an-dialog-actions an-fr-find-actions' });
183
184
  const prevBtn = createElement('button', { type: 'button', class: 'an-btn' });
184
- prevBtn.textContent = '\u2190 Prev';
185
+ prevBtn.textContent = L.prevBtn;
185
186
  const nextBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
186
- nextBtn.textContent = 'Next \u2192';
187
+ nextBtn.textContent = L.nextBtn;
187
188
  findActions.append(prevBtn, nextBtn);
188
189
  box.appendChild(findActions);
189
190
 
@@ -193,8 +194,8 @@ export class FindReplace {
193
194
  const replaceInput = createElement('input', {
194
195
  type: 'text',
195
196
  class: 'an-input',
196
- placeholder: 'Replace with\u2026',
197
- 'aria-label': 'Replace with',
197
+ placeholder: L.replacePlaceholder,
198
+ 'aria-label': L.replaceAriaLabel,
198
199
  });
199
200
  this._replaceInput = replaceInput;
200
201
  replaceRow.appendChild(replaceInput);
@@ -204,9 +205,9 @@ export class FindReplace {
204
205
  const replaceActions = createElement('div', { class: 'an-dialog-actions an-fr-replace-actions' });
205
206
  replaceActions.style.display = 'none';
206
207
  const replaceBtn = createElement('button', { type: 'button', class: 'an-btn' });
207
- replaceBtn.textContent = 'Replace';
208
+ replaceBtn.textContent = L.replaceBtn;
208
209
  const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
209
- replaceAllBtn.textContent = 'Replace All';
210
+ replaceAllBtn.textContent = L.replaceAllBtn;
210
211
  replaceActions.append(replaceBtn, replaceAllBtn);
211
212
  box.appendChild(replaceActions);
212
213
 
@@ -332,11 +332,12 @@ export class IconDialog {
332
332
  // ---------------------------------------------------------------------------
333
333
 
334
334
  _buildDialog() {
335
+ const L = this.context.locale.iconDialog;
335
336
  const overlay = createElement('div', {
336
337
  class: 'an-dialog-overlay',
337
338
  role: 'dialog',
338
339
  'aria-modal': 'true',
339
- 'aria-label': 'Insert FA icon',
340
+ 'aria-label': L.ariaLabel,
340
341
  });
341
342
 
342
343
  const box = createElement('div', { class: 'an-dialog-box an-icon-box' });
@@ -344,8 +345,8 @@ export class IconDialog {
344
345
  // Title row
345
346
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
346
347
  const title = createElement('h3', { class: 'an-dialog-title' });
347
- title.textContent = 'Insert FA Icon';
348
- const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': 'Close' });
348
+ title.textContent = L.title;
349
+ const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
349
350
  closeBtn.innerHTML = '×';
350
351
  titleRow.append(title, closeBtn);
351
352
 
@@ -353,7 +354,7 @@ export class IconDialog {
353
354
  const searchInput = createElement('input', {
354
355
  type: 'search',
355
356
  class: 'an-input an-icon-search',
356
- placeholder: 'Search icons…',
357
+ placeholder: L.searchPlaceholder,
357
358
  autocomplete: 'off',
358
359
  });
359
360
  this._searchInput = searchInput;
@@ -361,11 +362,11 @@ export class IconDialog {
361
362
  // Category tabs
362
363
  const catBar = createElement('div', { class: 'an-icon-cats' });
363
364
  const allTab = createElement('button', { type: 'button', class: 'an-icon-cat active', 'data-cat': 'all' });
364
- allTab.textContent = 'All';
365
+ allTab.textContent = L.all;
365
366
  catBar.appendChild(allTab);
366
367
  ICON_CATEGORIES.forEach(({ id, label }) => {
367
368
  const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
368
- tab.textContent = label;
369
+ tab.textContent = (L.categories && L.categories[id]) || label;
369
370
  catBar.appendChild(tab);
370
371
  });
371
372
  this._catBar = catBar;
@@ -386,7 +387,7 @@ export class IconDialog {
386
387
  const optRow = createElement('div', { class: 'an-icon-options' });
387
388
 
388
389
  const styleLabel = createElement('label', { class: 'an-label' });
389
- styleLabel.textContent = 'Style';
390
+ styleLabel.textContent = L.style;
390
391
  const styleSelect = createElement('select', { class: 'an-input an-icon-option-select' });
391
392
  [['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
392
393
  const opt = createElement('option', { value: v });
@@ -397,7 +398,7 @@ export class IconDialog {
397
398
  this._styleSelect = styleSelect;
398
399
 
399
400
  const sizeLabel = createElement('label', { class: 'an-label' });
400
- sizeLabel.textContent = 'Size';
401
+ sizeLabel.textContent = L.size;
401
402
  const sizeSelect = createElement('select', { class: 'an-input an-icon-option-select' });
402
403
  [['', 'Inherit'], ['0.75em', '0.75em'], ['1em', '1em'], ['1.25em', '1.25em'], ['1.5em', '1.5em'], ['2em', '2em'], ['3em', '3em']].forEach(([v, t]) => {
403
404
  const opt = createElement('option', { value: v });
@@ -408,30 +409,30 @@ export class IconDialog {
408
409
  this._sizeSelect = sizeSelect;
409
410
 
410
411
  const colorLabel = createElement('label', { class: 'an-label' });
411
- colorLabel.textContent = 'Color';
412
+ colorLabel.textContent = L.color;
412
413
  const colorInput = createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' });
413
414
  this._colorInput = colorInput;
414
415
 
415
416
  const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
416
417
  const useColorCb = createElement('input', { type: 'checkbox', checked: '' });
417
418
  this._useColorCb = useColorCb;
418
- useColorLabel.append(useColorCb, document.createTextNode(' Use color'));
419
+ useColorLabel.append(useColorCb, document.createTextNode(L.useColor));
419
420
 
420
421
  optRow.append(styleLabel, styleSelect, sizeLabel, sizeSelect, colorLabel, colorInput, useColorLabel);
421
422
 
422
423
  // Preview
423
424
  const preview = createElement('div', { class: 'an-icon-preview' });
424
425
  const previewHint = createElement('span', { class: 'an-icon-preview-hint' });
425
- previewHint.textContent = 'Select an icon';
426
+ previewHint.textContent = L.selectHint;
426
427
  preview.appendChild(previewHint);
427
428
  this._preview = preview;
428
429
 
429
430
  // Actions
430
431
  const btnRow = createElement('div', { class: 'an-dialog-actions' });
431
432
  const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary', disabled: '' });
432
- insertBtn.textContent = 'Insert FA Icon';
433
+ insertBtn.textContent = L.insertBtn;
433
434
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
434
- cancelBtn.textContent = 'Cancel';
435
+ cancelBtn.textContent = L.cancelBtn;
435
436
  btnRow.append(insertBtn, cancelBtn);
436
437
  this._insertBtn = insertBtn;
437
438
 
@@ -575,7 +576,18 @@ export class IconDialog {
575
576
 
576
577
  // 3. Insert the <i> node directly — never via execCommand/insertHTML
577
578
  // (execCommand leaves the caret inside the inserted element)
579
+ //
580
+ // E-1: When the range covers the entire content of a <td>/<th>,
581
+ // deleteContents() can drift the range endpoint outside the cell in some
582
+ // browsers. Save the cell reference first and re-anchor after deletion.
583
+ const _sc = range.startContainer;
584
+ const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
585
+ ?.closest?.('td, th');
578
586
  range.deleteContents();
587
+ if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
588
+ range.setStart(_tdAnchor, 0);
589
+ range.collapse(true);
590
+ }
579
591
  range.insertNode(iconEl);
580
592
 
581
593
  // 4. Place cursor after the icon.
@@ -57,35 +57,36 @@ export class ImageDialog {
57
57
  // ---------------------------------------------------------------------------
58
58
 
59
59
  _buildDialog() {
60
+ const L = this.context.locale.imageDialog;
60
61
  const overlay = createElement('div', {
61
62
  class: 'an-dialog-overlay',
62
63
  role: 'dialog',
63
64
  'aria-modal': 'true',
64
- 'aria-label': 'Insert image',
65
+ 'aria-label': L.ariaLabel,
65
66
  });
66
67
  const box = createElement('div', { class: 'an-dialog-box' });
67
68
 
68
69
  const title = createElement('h3', { class: 'an-dialog-title' });
69
- title.textContent = 'Insert Image';
70
+ title.textContent = L.title;
70
71
 
71
72
  // URL tab
72
73
  const urlLabel = createElement('label', { class: 'an-label' });
73
- urlLabel.textContent = 'Image URL';
74
+ urlLabel.textContent = L.imageUrl;
74
75
  const urlInput = createElement('input', {
75
76
  type: 'url',
76
77
  class: 'an-input',
77
- placeholder: 'https://example.com/image.png',
78
+ placeholder: L.urlPlaceholder,
78
79
  autocomplete: 'off',
79
80
  });
80
81
  this._urlInput = urlInput;
81
82
 
82
83
  // Alt text
83
84
  const altLabel = createElement('label', { class: 'an-label' });
84
- altLabel.textContent = 'Alt Text';
85
+ altLabel.textContent = L.altText;
85
86
  const altInput = createElement('input', {
86
87
  type: 'text',
87
88
  class: 'an-input',
88
- placeholder: 'Describe the image',
89
+ placeholder: L.altPlaceholder,
89
90
  autocomplete: 'off',
90
91
  });
91
92
  this._altInput = altInput;
@@ -94,13 +95,13 @@ export class ImageDialog {
94
95
 
95
96
  // Alignment
96
97
  const alignLabel = createElement('label', { class: 'an-label' });
97
- alignLabel.textContent = 'Alignment';
98
+ alignLabel.textContent = L.alignment;
98
99
  const alignRow = createElement('div', { class: 'an-align-row' });
99
100
  const alignOptions = [
100
- { value: '', label: 'None' },
101
- { value: 'left', label: 'Left' },
102
- { value: 'center', label: 'Center' },
103
- { value: 'right', label: 'Right' },
101
+ { value: '', label: L.alignNone },
102
+ { value: 'left', label: L.alignLeft },
103
+ { value: 'center', label: L.alignCenter },
104
+ { value: 'right', label: L.alignRight },
104
105
  ];
105
106
  alignOptions.forEach(({ value, label }) => {
106
107
  const radioId = `an-align-${value || 'none'}`;
@@ -114,11 +115,11 @@ export class ImageDialog {
114
115
  box.append(alignLabel, alignRow);
115
116
  if (this.options.allowImageUpload !== false) {
116
117
  const fileLabel = createElement('label', { class: 'an-label' });
117
- fileLabel.textContent = 'Or upload a file';
118
+ fileLabel.textContent = L.uploadLabel;
118
119
  const fileInput = createElement('input', {
119
120
  type: 'file',
120
121
  class: 'an-input',
121
- accept: 'image/*',
122
+ accept: 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/avif',
122
123
  });
123
124
  this._fileInput = fileInput;
124
125
  // Hint line shown below the file input for format errors
@@ -132,9 +133,9 @@ export class ImageDialog {
132
133
  // Buttons
133
134
  const btnRow = createElement('div', { class: 'an-dialog-actions' });
134
135
  const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
135
- insertBtn.textContent = 'Insert';
136
+ insertBtn.textContent = L.insertBtn;
136
137
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
137
- cancelBtn.textContent = 'Cancel';
138
+ cancelBtn.textContent = L.cancelBtn;
138
139
  btnRow.appendChild(insertBtn);
139
140
  btnRow.appendChild(cancelBtn);
140
141
 
@@ -159,10 +160,14 @@ export class ImageDialog {
159
160
  const file = this._fileInput && this._fileInput.files && this._fileInput.files[0];
160
161
  if (!file || !file.type.startsWith('image/')) return;
161
162
 
162
- // C2: Reject image formats browsers cannot display (TIFF, BMP, etc.).
163
- const UNSUPPORTED = ['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp'];
164
- if (UNSUPPORTED.includes(file.type)) {
165
- const message = `Format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
163
+ // C2: Only allow web-displayable formats. TIFF, BMP, RAW and similar
164
+ // formats are not rendered by browsers — reject them with a clear message.
165
+ const SUPPORTED = new Set([
166
+ 'image/jpeg', 'image/png', 'image/gif',
167
+ 'image/webp', 'image/svg+xml', 'image/avif',
168
+ ]);
169
+ if (!SUPPORTED.has(file.type)) {
170
+ const message = this.context.locale.errors.imageFormat(file.type);
166
171
  if (this._fileHint) this._fileHint.textContent = message;
167
172
  this.context.triggerEvent('imageError', { file, message });
168
173
  this._fileInput.value = '';
@@ -173,7 +178,7 @@ export class ImageDialog {
173
178
  // Validate file size (max 5 MB by default)
174
179
  const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
175
180
  if (file.size > maxSize) {
176
- const message = `Image file is too large. Maximum allowed size is ${this.options.maxImageSize || 5} MB.`;
181
+ const message = this.context.locale.errors.imageSize(this.options.maxImageSize || 5);
177
182
  if (this._fileHint) this._fileHint.textContent = message;
178
183
  console.warn('[AutumnNote] ImageDialog:', message);
179
184
  this.context.triggerEvent('imageError', { file, message });
@@ -75,24 +75,25 @@ export class ImageTooltip {
75
75
  // ---------------------------------------------------------------------------
76
76
 
77
77
  _buildTooltip() {
78
+ const L = this.context.locale.tooltips.image;
78
79
  const el = createElement('div', {
79
80
  class: 'an-link-tooltip an-image-tooltip',
80
81
  role: 'toolbar',
81
- 'aria-label': 'Image actions',
82
+ 'aria-label': L.ariaLabel,
82
83
  });
83
84
  el.style.display = 'none';
84
85
 
85
86
  // Label showing "Image"
86
87
  this._label = createElement('span', { class: 'an-link-tooltip-url' });
87
- this._label.textContent = 'Image';
88
+ this._label.textContent = L.label;
88
89
  el.appendChild(this._label);
89
90
 
90
91
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
91
92
 
92
- this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, 'Float Left', () => this._setFloat('left'));
93
- this._floatNoneBtn = this._makeBtn(ICONS.floatNone, 'No Float', () => this._setFloat(''));
94
- this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, 'Align Center', () => this._setCenter());
95
- this._floatRightBtn = this._makeBtn(ICONS.floatRight, 'Float Right', () => this._setFloat('right'));
93
+ this._floatLeftBtn = this._makeBtn(ICONS.floatLeft, L.floatLeft, () => this._setFloat('left'));
94
+ this._floatNoneBtn = this._makeBtn(ICONS.floatNone, L.noFloat, () => this._setFloat(''));
95
+ this._alignCenterBtn = this._makeBtn(ICONS.alignCenter, L.alignCenter, () => this._setCenter());
96
+ this._floatRightBtn = this._makeBtn(ICONS.floatRight, L.floatRight, () => this._setFloat('right'));
96
97
 
97
98
  el.appendChild(this._floatLeftBtn);
98
99
  el.appendChild(this._floatNoneBtn);
@@ -101,28 +102,28 @@ export class ImageTooltip {
101
102
 
102
103
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
103
104
 
104
- this._originalBtn = this._makeBtn(ICONS.originalSize, 'Original Size', () => this._resetSize());
105
+ this._originalBtn = this._makeBtn(ICONS.originalSize, L.originalSize, () => this._resetSize());
105
106
 
106
107
  el.appendChild(this._originalBtn);
107
108
 
108
109
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
109
110
 
110
- el.appendChild(this._makeBtn(ICONS.rotateLeft, 'Rotate Left', () => this._rotate(-90)));
111
- el.appendChild(this._makeBtn(ICONS.rotateRight, 'Rotate Right', () => this._rotate(90)));
111
+ el.appendChild(this._makeBtn(ICONS.rotateLeft, L.rotateLeft, () => this._rotate(-90)));
112
+ el.appendChild(this._makeBtn(ICONS.rotateRight, L.rotateRight, () => this._rotate(90)));
112
113
 
113
114
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
114
115
 
115
- this._cropBtn = this._makeBtn(ICONS.crop, 'Crop Image', () => this._crop());
116
+ this._cropBtn = this._makeBtn(ICONS.crop, L.cropImage, () => this._crop());
116
117
  el.appendChild(this._cropBtn);
117
118
 
118
119
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
119
120
 
120
- this._captionBtn = this._makeBtn(ICONS.caption, 'Add / Edit Caption', () => this._toggleCaption());
121
+ this._captionBtn = this._makeBtn(ICONS.caption, L.addCaption, () => this._toggleCaption());
121
122
  el.appendChild(this._captionBtn);
122
123
 
123
124
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
124
125
 
125
- this._deleteBtn = this._makeBtn(ICONS.deleteImg, 'Delete Image', () => this._delete(), true);
126
+ this._deleteBtn = this._makeBtn(ICONS.deleteImg, L.deleteImage, () => this._delete(), true);
126
127
 
127
128
  el.appendChild(this._deleteBtn);
128
129
 
@@ -59,19 +59,20 @@ export class LinkDialog {
59
59
  // ---------------------------------------------------------------------------
60
60
 
61
61
  _buildDialog() {
62
- const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Insert link' });
62
+ const L = this.context.locale.linkDialog;
63
+ const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': L.ariaLabel });
63
64
  const box = createElement('div', { class: 'an-dialog-box' });
64
65
 
65
66
  const title = createElement('h3', { class: 'an-dialog-title' });
66
- title.textContent = 'Insert Link';
67
+ title.textContent = L.title;
67
68
 
68
69
  // URL field
69
70
  const urlLabel = createElement('label', { class: 'an-label' });
70
- urlLabel.textContent = 'URL';
71
+ urlLabel.textContent = L.url;
71
72
  const urlInput = createElement('input', {
72
73
  type: 'url',
73
74
  class: 'an-input',
74
- placeholder: 'https://',
75
+ placeholder: L.urlPlaceholder,
75
76
  id: 'an-link-url',
76
77
  name: 'url',
77
78
  autocomplete: 'off',
@@ -80,11 +81,11 @@ export class LinkDialog {
80
81
 
81
82
  // Text field
82
83
  const textLabel = createElement('label', { class: 'an-label' });
83
- textLabel.textContent = 'Display Text';
84
+ textLabel.textContent = L.displayText;
84
85
  const textInput = createElement('input', {
85
86
  type: 'text',
86
87
  class: 'an-input',
87
- placeholder: 'Link text',
88
+ placeholder: L.textPlaceholder,
88
89
  id: 'an-link-text',
89
90
  name: 'linkText',
90
91
  autocomplete: 'off',
@@ -100,14 +101,14 @@ export class LinkDialog {
100
101
  });
101
102
  this._tabCheckbox = tabCheckbox;
102
103
  tabLabel.appendChild(tabCheckbox);
103
- tabLabel.appendChild(document.createTextNode(' Open in new tab'));
104
+ tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
104
105
 
105
106
  // Buttons
106
107
  const btnRow = createElement('div', { class: 'an-dialog-actions' });
107
108
  const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
108
- insertBtn.textContent = 'Insert';
109
+ insertBtn.textContent = L.insertBtn;
109
110
  const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
110
- cancelBtn.textContent = 'Cancel';
111
+ cancelBtn.textContent = L.cancelBtn;
111
112
  btnRow.appendChild(insertBtn);
112
113
  btnRow.appendChild(cancelBtn);
113
114
 
@@ -63,7 +63,8 @@ export class LinkTooltip {
63
63
  // ---------------------------------------------------------------------------
64
64
 
65
65
  _buildTooltip() {
66
- const el = createElement('div', { class: 'an-link-tooltip', role: 'toolbar', 'aria-label': 'Link actions' });
66
+ const L = this.context.locale.tooltips.link;
67
+ const el = createElement('div', { class: 'an-link-tooltip', role: 'toolbar', 'aria-label': L.ariaLabel });
67
68
  el.style.display = 'none';
68
69
 
69
70
  // URL preview text (truncated)
@@ -74,10 +75,10 @@ export class LinkTooltip {
74
75
  el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
75
76
 
76
77
  // Action buttons
77
- this._openBtn = this._makeBtn(ICONS.open, 'Open link', () => this._openLink());
78
- this._copyBtn = this._makeBtn(ICONS.copy, 'Copy URL', () => this._copyLink());
79
- this._editBtn = this._makeBtn(ICONS.edit, 'Edit link', () => this._editLink());
80
- this._unlinkBtn = this._makeBtn(ICONS.unlink, 'Remove link', () => this._unlink());
78
+ this._openBtn = this._makeBtn(ICONS.open, L.openLink, () => this._openLink());
79
+ this._copyBtn = this._makeBtn(ICONS.copy, L.copyUrl, () => this._copyLink());
80
+ this._editBtn = this._makeBtn(ICONS.edit, L.editLink, () => this._editLink());
81
+ this._unlinkBtn = this._makeBtn(ICONS.unlink, L.removeLink, () => this._unlink());
81
82
 
82
83
  el.appendChild(this._openBtn);
83
84
  el.appendChild(this._copyBtn);
@@ -39,7 +39,12 @@ export class Placeholder {
39
39
  _update() {
40
40
  const editable = this.context.layoutInfo.editable;
41
41
  const isFocused = document.activeElement === editable;
42
- const isEmpty = !editable.textContent.trim() &&
42
+ // Strip ZWS (\u200B) cursor anchors used by checklist/icon insertion in
43
+ // addition to regular whitespace before deciding if the editor is empty.
44
+ // Without this, a freshly-created checklist item or icon leaves a ZWS in
45
+ // the DOM that causes the placeholder to overlap real content (A-1).
46
+ const hasText = editable.textContent.replace(/\u200B/g, '').trim().length > 0;
47
+ const isEmpty = !hasText &&
43
48
  !editable.querySelector('img, table, hr, .an-video-wrapper');
44
49
  editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
45
50
  }
@@ -93,7 +93,7 @@ export class ShortcutsDialog {
93
93
  class: 'an-dialog-overlay',
94
94
  role: 'dialog',
95
95
  'aria-modal': 'true',
96
- 'aria-label': 'Keyboard Shortcuts',
96
+ 'aria-label': this.context.locale.shortcutsDialog.ariaLabel,
97
97
  });
98
98
 
99
99
  const box = createElement('div', { class: 'an-dialog-box an-shortcuts-box' });
@@ -101,18 +101,19 @@ export class ShortcutsDialog {
101
101
  // Title row with close button
102
102
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
103
103
  const title = createElement('h3', { class: 'an-dialog-title' });
104
- title.textContent = 'Keyboard Shortcuts';
104
+ title.textContent = this.context.locale.shortcutsDialog.title;
105
105
  const closeBtn = createElement('button', {
106
106
  type: 'button',
107
107
  class: 'an-icon-close',
108
- 'aria-label': 'Close',
108
+ 'aria-label': this.context.locale.shortcutsDialog.close,
109
109
  });
110
110
  closeBtn.textContent = '×';
111
111
  this._closeBtn = closeBtn;
112
112
  titleRow.append(title, closeBtn);
113
113
  box.appendChild(titleRow);
114
114
 
115
- SHORTCUTS.forEach(({ category, items }) => {
115
+ const shortcuts = this.context.locale.shortcutsDialog.shortcuts || SHORTCUTS;
116
+ shortcuts.forEach(({ category, items }) => {
116
117
  const catEl = createElement('div', { class: 'an-shortcuts-cat' });
117
118
  catEl.textContent = category;
118
119
  box.appendChild(catEl);
@@ -82,7 +82,7 @@ export class Statusbar {
82
82
  if (this.options.resizable !== false) {
83
83
  const handle = createElement('div', {
84
84
  class: 'an-resize-handle',
85
- title: 'Resize editor',
85
+ title: this.context.locale.statusbar.resizeHandle,
86
86
  'aria-hidden': 'true',
87
87
  });
88
88
  this._bindResize(handle);
@@ -220,12 +220,13 @@ export class Statusbar {
220
220
  const maxWords = this.options.maxWords || 0;
221
221
  const maxChars = this.options.maxChars || 0;
222
222
 
223
+ const LS = this.context.locale.statusbar;
223
224
  this._wordCountEl.textContent = maxWords
224
- ? `Words: ${words}/${maxWords}`
225
- : `Words: ${words}`;
225
+ ? LS.wordsLimit(words, maxWords)
226
+ : LS.words(words);
226
227
  this._charCountEl.textContent = maxChars
227
- ? `Chars: ${chars}/${maxChars}`
228
- : `Chars: ${chars}`;
228
+ ? LS.charsLimit(chars, maxChars)
229
+ : LS.chars(chars);
229
230
 
230
231
  // Apply warning / exceeded styles
231
232
  _applyLimitClass(this._wordCountEl, words, maxWords);