diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ff4d2..c783464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,8 @@ - Primary toolbar plus an expandable extended toolbar (horizontal scroll on narrow windows) - Status bar with page number, word count, language and zoom controls - About dialog +- UI internationalization (English / German): language picker, auto-detects the browser + language on first load, persisted; covers toolbar, dialogs, menus, status bar and warnings **File & export** - ODT export via odf-kit with extensive content.xml / styles.xml post-processing (custom node types, tables with borders, images, fonts and colors); filename derived from the first heading (fallback `document.odt`) @@ -91,7 +93,6 @@ Other: - Custom right-click context menu (formatting, cut/copy) - Multi-document management - PWA / offline support -- i18n - ... ### Known issues / deferred diff --git a/src/App.svelte b/src/App.svelte index eab6fbd..d290a25 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -24,6 +24,9 @@ import LanguagePicker from './lib/components/LanguagePicker.svelte'; import UiLanguagePicker from './lib/components/UiLanguagePicker.svelte'; import AboutDialog from './lib/components/AboutDialog.svelte'; + import { t } from './lib/i18n/i18n.svelte'; + import { withShortcut } from './lib/i18n/shortcut'; + import { localizeImportMessage } from './lib/i18n/importMessages'; let editor: Editor | null = $state(null); let tick: number = $state(0); @@ -96,9 +99,9 @@ // Only computed while the field is blank (otherwise the placeholder is hidden, // so we skip the per-transaction getJSON). let namePlaceholder = $derived.by(() => { - if (documentName.trim() || tick < 0 || !editor) return 'Untitled document'; + if (documentName.trim() || tick < 0 || !editor) return t().app.untitled; const base = stripOdtExtension(deriveFilename(editor.getJSON() as Parameters[0])); - return base === 'document' ? 'Untitled document' : base; + return base === 'document' ? t().app.untitled : base; }); function suggestedFilename(json: Parameters[0]): string { @@ -298,7 +301,7 @@ function handleNew() { if (!editor) return; - if (isDocNonEmpty() && !confirm('Start a new document? Any unsaved changes will be lost.')) return; + if (isDocNonEmpty() && !confirm(t().dialogs.confirmNew)) return; editor.commands.setContent('

'); // onUpdate fires → autosave // Reset everything to defaults; the $effects persist these. hfActive = null; @@ -321,7 +324,7 @@ const result = isDocx ? importDocx(bytes) : importOdt(bytes); const hasContent = editor.state.doc.textContent.length > 0 || editor.state.doc.childCount > 1; - if (hasContent && !confirm('Opening this file will replace the current document. Continue?')) { + if (hasContent && !confirm(t().dialogs.confirmReplace)) { return; } @@ -347,11 +350,11 @@ if (result.warnings.length) { console.warn('[import] Unsupported content in opened file:', result.warnings); - alert(`Opened with limitations:\n• ${result.warnings.join('\n• ')}`); + alert(t().dialogs.openedWithLimitations(result.warnings.map(localizeImportMessage).join('\n• '))); } } catch (err) { console.error('[import] Failed to open file:', err); - alert(err instanceof Error ? err.message : 'Could not open this file.'); + alert(err instanceof Error ? localizeImportMessage(err.message) : t().dialogs.couldNotOpen); } } @@ -364,7 +367,7 @@ } catch (err) { if ((err as DOMException)?.name !== 'AbortError') { console.error('[open] Failed to open file:', err); - alert('Could not open this file.'); + alert(t().dialogs.couldNotOpen); } } } else { @@ -392,7 +395,7 @@ // A stored handle may have lost permission; re-prompt via Save As. if (fileHandle) { fileHandle = null; await handleSaveAs(); return; } console.error('[save] Failed to save file:', err); - alert('Could not save this file.'); + alert(t().dialogs.couldNotSave); } } @@ -406,7 +409,7 @@ } catch (err) { if ((err as DOMException)?.name === 'AbortError') return; console.error('[save] Failed to save file:', err); - alert('Could not save this file.'); + alert(t().dialogs.couldNotSave); } } @@ -424,7 +427,7 @@ } catch (err) { if ((err as DOMException)?.name === 'AbortError') return; console.error('[docx] Export failed:', err); - alert('Could not export to Word (.docx).'); + alert(t().dialogs.couldNotExportDocx); } finally { docxBusy = false; } @@ -447,7 +450,7 @@ }); } catch (err) { console.error('[pdf] Export failed:', err); - alert('Could not export to PDF.'); + alert(t().dialogs.couldNotExportPdf); } finally { pdfBusy = false; } @@ -471,7 +474,7 @@ }); } catch (err) { console.error('[pdf] Print failed:', err); - alert('Could not print.'); + alert(t().dialogs.couldNotPrint); } finally { pdfBusy = false; } @@ -494,7 +497,7 @@ }); } catch (err) { console.error('[pdf] Print failed:', err); - alert('Could not print to PDF.'); + alert(t().dialogs.couldNotPrintPdf); } } @@ -592,7 +595,7 @@
- @@ -618,13 +621,13 @@ type="text" bind:value={documentName} placeholder={namePlaceholder} - title="Document name" + title={t().app.documentName} onkeydown={(e) => e.key === 'Enter' && (e.currentTarget as HTMLInputElement).blur()} onblur={() => (documentName = documentName.trim())} />
- -
{#if exportMenuOpen} {/if}
- {/each} @@ -761,7 +764,7 @@ class="expand-toggle" class:active={toolbarExpanded} onclick={toggleToolbar} - title={toolbarExpanded ? 'Hide extra tools' : 'Show extra tools'} + title={toolbarExpanded ? t().app.hideExtraTools : t().app.showExtraTools} aria-expanded={toolbarExpanded} > @@ -773,7 +776,7 @@ - Tools + {t().app.tools} @@ -821,37 +824,37 @@ {/if}
- Page {currentPage} of {numPages} + {t().status.pageOf(currentPage, numPages)}
{#if wordCountOpen} - @@ -861,7 +864,7 @@
- + setZoom(parseInt((e.target as HTMLInputElement).value, 10))} - title="Zoom" + title={t().status.zoom} /> - - + +
@@ -1204,10 +1207,10 @@ top, but below the basic toolbar (200) so its dropdowns open over this row; the header menus sit above everything via a higher z-index. */ z-index: 160; - /* The toggle is absolute (adds no height): collapsed the bar is 0-height so the - document fills up to here; expanded it grows with the extended toolbar. - padding-left reserves the tab's column. */ - padding: 0 1rem 0 6.5rem; + /* The toggle is absolute while collapsed (adds no height) so the bar is 0-height + and the document fills up to here; expanded, the toggle joins the flex flow + (rule below) and the extended toolbar follows it — no reserved column needed. */ + padding: 0 1rem; background: transparent; border: 1px solid transparent; border-top: none; @@ -1265,6 +1268,16 @@ box-shadow: var(--shadow); } + /* Expanded: the toggle sits in the flex flow so its width is content-driven (any + language) and the extended toolbar starts right after it, never overlapping. */ + .toolbar-secondary.expanded .expand-toggle { + position: relative; + left: auto; + top: auto; + flex: none; + margin: 0 0.6rem 0 0.35rem; + } + .expand-toggle:hover { border-color: var(--color-primary); } diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index f6b6916..1b0dd49 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -22,6 +22,8 @@ import { DEFAULT_HF_DISTANCES, type HfDoc, type HfZone, type HfDistances } from '../storage/headerFooter'; import { FORCE_PAGE_RECALC, type TableBreakBand } from '../editor/extensions/pageBreaks'; import { recordTransaction, resetHistoryLog } from '../utils/historyLog.svelte'; + import { t } from '../i18n/i18n.svelte'; + import { withShortcut } from '../i18n/shortcut'; import '../../styles/editor.css'; const DEFAULT_EDITOR_FONT = 'Georgia'; // must match ToolbarExpanded.svelte @@ -159,8 +161,6 @@ // --- Link hover hint (Word/LibreOffice style) --- // Hovering a link shows its URL + a modifier-click hint; the link only follows on // Ctrl/Cmd+click (handleClick in editorProps), so a plain click can edit the text. - const linkHintKey = - typeof navigator !== 'undefined' && /Mac|iP(hone|ad|od)/.test(navigator.platform) ? '⌘' : 'Ctrl'; let linkTip = $state<{ top: number; left: number; href: string } | null>(null); function showLinkTip(a: HTMLAnchorElement) { @@ -733,7 +733,7 @@ {#if linkTip} {/if} diff --git a/src/lib/components/LanguagePicker.svelte b/src/lib/components/LanguagePicker.svelte index ef961fe..fe272d9 100644 --- a/src/lib/components/LanguagePicker.svelte +++ b/src/lib/components/LanguagePicker.svelte @@ -1,5 +1,6 @@ -