feat: expand i18n catalog with submenues of images, table, ...
This commit is contained in:
1 parent
2cbc0edae1
commit
9dcf0e246c
15 files changed
+461
-157
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
// Placeholder until the project's repository is public; swap the URL when known.
|
||||
const REPO_URL = 'https://github.com/your-org/edentext';
|
||||
|
||||
@@ -31,10 +32,10 @@
|
||||
bind:this={dialogEl}
|
||||
onclose={onClose}
|
||||
onclick={onClick}
|
||||
aria-label="About EdenText"
|
||||
aria-label={t().about.label}
|
||||
>
|
||||
<div class="card">
|
||||
<button class="close" onclick={() => (open = false)} aria-label="Close" title="Close">
|
||||
<button class="close" onclick={() => (open = false)} aria-label={t().common.close} title={t().common.close}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
@@ -42,15 +43,15 @@
|
||||
|
||||
<img class="logo" src="/EdenText.png" alt="EdenText" />
|
||||
|
||||
<p class="tagline">A private editor for OpenDocument files — runs entirely in your browser.</p>
|
||||
<p class="tagline">{t().about.tagline}</p>
|
||||
|
||||
<p class="version">Version {__APP_VERSION__}</p>
|
||||
<p class="version">{t().about.version(__APP_VERSION__)}</p>
|
||||
|
||||
<a class="repo" href={REPO_URL} target="_blank" rel="noopener">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 .2a8 8 0 0 0-2.53 15.59c.4.07.55-.17.55-.38l-.01-1.34c-2.23.49-2.7-1.07-2.7-1.07-.36-.93-.89-1.18-.89-1.18-.72-.5.06-.49.06-.49.8.06 1.22.83 1.22.83.71 1.22 1.87.87 2.33.66.07-.52.28-.87.5-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.83-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.22 2.2.82a7.6 7.6 0 0 1 4 0c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.52.56.83 1.28.83 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48l-.01 2.19c0 .21.15.46.55.38A8 8 0 0 0 8 .2z" />
|
||||
</svg>
|
||||
<span>View on GitHub</span>
|
||||
<span>{t().about.viewOnGitHub}</span>
|
||||
</a>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onDestroy, untrack } from 'svelte';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
editor,
|
||||
@@ -9,7 +10,7 @@
|
||||
defaultColor,
|
||||
title,
|
||||
chevronTitle,
|
||||
clearLabel = 'Automatic',
|
||||
clearLabel,
|
||||
open = $bindable(false),
|
||||
onApply,
|
||||
onClear,
|
||||
@@ -377,7 +378,7 @@
|
||||
class="color-automatic"
|
||||
class:active={currentColor === null}
|
||||
onclick={clearColor}
|
||||
>{clearLabel}</button>
|
||||
>{clearLabel ?? t().color.automatic}</button>
|
||||
<div class="color-grid">
|
||||
{#each COLOR_PALETTE as row}
|
||||
<div class="color-row">
|
||||
@@ -395,12 +396,12 @@
|
||||
{/each}
|
||||
</div>
|
||||
<div class="color-extras">
|
||||
<button class="color-more-trigger" onclick={openMoreColors}>More colors…</button>
|
||||
<button class="color-more-trigger" onclick={openMoreColors}>{t().color.moreColors}</button>
|
||||
<button
|
||||
class="color-pipette-trigger"
|
||||
onclick={pickFromPage}
|
||||
title="Pick a color from the page"
|
||||
aria-label="Pick a color from the page"
|
||||
title={t().color.pickFromPage}
|
||||
aria-label={t().color.pickFromPage}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<g transform="rotate(-45 8 8)" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round" stroke-linecap="round">
|
||||
@@ -414,7 +415,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if open && view === 'custom'}
|
||||
<div class="color-window" role="dialog" aria-label="Choose color">
|
||||
<div class="color-window" role="dialog" aria-label={t().color.chooseColor}>
|
||||
<div class="color-window-body">
|
||||
<div
|
||||
class="sv-square"
|
||||
@@ -437,8 +438,8 @@
|
||||
<input class="color-window-hex" type="text" value={stagedColor} oninput={onHexInput} maxlength="7" />
|
||||
</div>
|
||||
<div class="color-more-actions">
|
||||
<button class="color-cancel" onclick={cancelMoreColors}>Cancel</button>
|
||||
<button class="color-confirm" onclick={confirmMoreColors}>OK</button>
|
||||
<button class="color-cancel" onclick={cancelMoreColors}>{t().common.cancel}</button>
|
||||
<button class="color-confirm" onclick={confirmMoreColors}>{t().common.ok}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { onMount } from 'svelte';
|
||||
import { getSearchState } from '../editor/extensions/searchReplace';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import { withShortcut } from '../i18n/shortcut';
|
||||
|
||||
let { editor, tick, mode, focusNonce, onClose }:
|
||||
{ editor: Editor | null; tick: number; mode: 'find' | 'replace'; focusNonce: number; onClose: () => void } = $props();
|
||||
@@ -62,8 +64,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="find-bar" role="dialog" aria-label="Find and replace">
|
||||
<button class="fb-expand" onclick={() => (showReplace = !showReplace)} title={showReplace ? 'Hide replace' : 'Show replace'} aria-label="Toggle replace">
|
||||
<div class="find-bar" role="dialog" aria-label={t().findReplace.dialogLabel}>
|
||||
<button class="fb-expand" onclick={() => (showReplace = !showReplace)} title={showReplace ? t().findReplace.hideReplace : t().findReplace.showReplace} aria-label={t().findReplace.toggleReplace}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" style="transform: rotate({showReplace ? 90 : 0}deg)">
|
||||
<path d="M3 1.5l3.5 3.5L3 8.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -77,26 +79,26 @@
|
||||
oninput={applySearch}
|
||||
onkeydown={onFindKeydown}
|
||||
type="text"
|
||||
placeholder="Find"
|
||||
placeholder={t().findReplace.findPlaceholder}
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<span class="fb-count">
|
||||
{#if findText && results.count === 0}No results
|
||||
{#if findText && results.count === 0}{t().findReplace.noResults}
|
||||
{:else if results.count > 0}{results.current + 1}/{results.count}
|
||||
{/if}
|
||||
</span>
|
||||
<button class="fb-opt" class:on={matchCase} onclick={toggleCase} title="Match case" aria-pressed={matchCase}>Aa</button>
|
||||
<button class="fb-opt fb-word" class:on={wholeWord} onclick={toggleWord} title="Whole word" aria-pressed={wholeWord}>
|
||||
<button class="fb-opt" class:on={matchCase} onclick={toggleCase} title={t().findReplace.matchCase} aria-pressed={matchCase}>Aa</button>
|
||||
<button class="fb-opt fb-word" class:on={wholeWord} onclick={toggleWord} title={t().findReplace.wholeWord} aria-pressed={wholeWord}>
|
||||
<span>W</span>
|
||||
</button>
|
||||
<button class="fb-nav" onclick={prev} disabled={!results.count} title="Previous (Shift+Enter)" aria-label="Previous match">
|
||||
<button class="fb-nav" onclick={prev} disabled={!results.count} title={`${t().findReplace.previous} (${withShortcut('Shift+Enter')})`} aria-label={t().findReplace.previousAria}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2.5 7.5L6 4l3.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button class="fb-nav" onclick={next} disabled={!results.count} title="Next (Enter)" aria-label="Next match">
|
||||
<button class="fb-nav" onclick={next} disabled={!results.count} title={`${t().findReplace.next} (Enter)`} aria-label={t().findReplace.nextAria}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2.5 4.5L6 8l3.5-3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
<button class="fb-close" onclick={onClose} title="Close (Esc)" aria-label="Close">
|
||||
<button class="fb-close" onclick={onClose} title={`${t().common.close} (Esc)`} aria-label={t().common.close}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -107,12 +109,12 @@
|
||||
bind:value={replaceText}
|
||||
onkeydown={onReplaceKeydown}
|
||||
type="text"
|
||||
placeholder="Replace"
|
||||
placeholder={t().findReplace.replacePlaceholder}
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button class="fb-text-btn" onclick={replaceOne} disabled={!results.count} title="Replace current match">Replace</button>
|
||||
<button class="fb-text-btn" onclick={replaceEvery} disabled={!results.count} title="Replace all matches">All</button>
|
||||
<button class="fb-text-btn" onclick={replaceOne} disabled={!results.count} title={t().findReplace.replaceCurrent}>{t().findReplace.replace}</button>
|
||||
<button class="fb-text-btn" onclick={replaceEvery} disabled={!results.count} title={t().findReplace.replaceAllTitle}>{t().findReplace.all}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { hfIsEmpty, DEFAULT_HF_DISTANCES, type HfDoc, type HfZone, type HfDistances } from '../storage/headerFooter';
|
||||
import { cmToPx, type PageMargins } from '../storage/pageMargins';
|
||||
import { PAGE_W_PORTRAIT, PAGE_H_PORTRAIT, type Orientation } from '../storage/pageOrientation';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
headerDoc = $bindable(),
|
||||
@@ -130,7 +131,7 @@
|
||||
liveZone = zone;
|
||||
const ed = new Editor({
|
||||
element: mount,
|
||||
extensions: hfExtensions(zone === 'header' ? 'Header…' : 'Footer…'),
|
||||
extensions: hfExtensions(zone === 'header' ? t().hf.headerPlaceholder : t().hf.footerPlaceholder),
|
||||
content: (zoneDoc(zone) ?? emptyDoc()) as Content,
|
||||
// No autofocus: its scrollIntoView nudges the page so the just-clicked zone
|
||||
// appears to jump. Focus the zone explicitly without scrolling instead.
|
||||
@@ -180,7 +181,7 @@
|
||||
<div
|
||||
class="hf-zone hf-{zone}"
|
||||
class:hf-empty={!html}
|
||||
data-hf-label={zone === 'header' ? 'Double-click to add a header' : 'Double-click to add a footer'}
|
||||
data-hf-label={zone === 'header' ? t().hf.addHeaderHint : t().hf.addFooterHint}
|
||||
style={boxStyle(zoneBox(zone, p))}
|
||||
ondblclick={() => startEdit(zone, p)}
|
||||
role="button"
|
||||
@@ -197,11 +198,11 @@
|
||||
{@const box = zoneBox(hfActive, editingPage)}
|
||||
<div class="hf-zone hf-{hfActive} hf-active" style={boxStyle(box)} bind:this={liveMount}></div>
|
||||
<div class="hf-bar" style="top: {box.top}px; left: {box.left + box.width}px;">
|
||||
<span class="hf-bar-label">Insert</span>
|
||||
<button class="hf-bar-btn" title="Insert the current page number" onmousedown={(e) => e.preventDefault()} onclick={() => insertField('pageNumber')}>Page number</button>
|
||||
<button class="hf-bar-btn" title="Insert the total page count" onmousedown={(e) => e.preventDefault()} onclick={() => insertField('pageCount')}>Page count</button>
|
||||
<span class="hf-bar-label">{t().hf.insert}</span>
|
||||
<button class="hf-bar-btn" title={t().hf.pageNumberTitle} onmousedown={(e) => e.preventDefault()} onclick={() => insertField('pageNumber')}>{t().hf.pageNumber}</button>
|
||||
<button class="hf-bar-btn" title={t().hf.pageCountTitle} onmousedown={(e) => e.preventDefault()} onclick={() => insertField('pageCount')}>{t().hf.pageCount}</button>
|
||||
<span class="hf-bar-sep"></span>
|
||||
<button class="hf-bar-btn hf-bar-done" title="Finish editing the header/footer" onmousedown={(e) => e.preventDefault()} onclick={() => (hfActive = null)}>Done</button>
|
||||
<button class="hf-bar-btn hf-bar-done" title={t().hf.doneTitle} onmousedown={(e) => e.preventDefault()} onclick={() => (hfActive = null)}>{t().hf.done}</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import type { WrapMode } from '../editor/extensions/image';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
editor,
|
||||
@@ -20,12 +21,13 @@
|
||||
editor?.chain().focus().setImageWrap(mode).run();
|
||||
}
|
||||
|
||||
const modes: { mode: WrapMode; title: string }[] = [
|
||||
{ mode: 'inline', title: 'In line with text' },
|
||||
{ mode: 'left', title: 'Wrap text — image left' },
|
||||
{ mode: 'right', title: 'Wrap text — image right' },
|
||||
{ mode: 'topBottom', title: 'Top and bottom' },
|
||||
];
|
||||
const modes: WrapMode[] = ['inline', 'left', 'right', 'topBottom'];
|
||||
function wrapTitle(m: WrapMode): string {
|
||||
return m === 'inline' ? t().image.wrapInline
|
||||
: m === 'left' ? t().image.wrapLeft
|
||||
: m === 'right' ? t().image.wrapRight
|
||||
: t().image.wrapTopBottom;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -33,19 +35,19 @@
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="toolbar"
|
||||
tabindex="-1"
|
||||
aria-label="Image text wrap"
|
||||
aria-label={t().image.wrapToolbar}
|
||||
onmousedown={(e) => e.preventDefault()}
|
||||
>
|
||||
{#each modes as m}
|
||||
<button
|
||||
class="it-btn"
|
||||
class:active={wrap === m.mode}
|
||||
title={m.title}
|
||||
aria-label={m.title}
|
||||
aria-pressed={wrap === m.mode}
|
||||
onclick={() => set(m.mode)}
|
||||
class:active={wrap === m}
|
||||
title={wrapTitle(m)}
|
||||
aria-label={wrapTitle(m)}
|
||||
aria-pressed={wrap === m}
|
||||
onclick={() => set(m)}
|
||||
>
|
||||
{#if m.mode === 'inline'}
|
||||
{#if m === 'inline'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<line x1="2.5" y1="3" x2="15.5" y2="3" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2.5" y1="12" x2="5.5" y2="12" stroke="currentColor" stroke-width="1.2" />
|
||||
@@ -53,7 +55,7 @@
|
||||
<line x1="12.5" y1="12" x2="15.5" y2="12" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2.5" y1="15" x2="15.5" y2="15" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{:else if m.mode === 'left'}
|
||||
{:else if m === 'left'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<line x1="2.5" y1="3" x2="15.5" y2="3" stroke="currentColor" stroke-width="1.2" />
|
||||
<rect x="2.5" y="6" width="6" height="6" rx="1" fill="currentColor" />
|
||||
@@ -61,7 +63,7 @@
|
||||
<line x1="10" y1="10" x2="15.5" y2="10" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2.5" y1="15" x2="15.5" y2="15" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{:else if m.mode === 'right'}
|
||||
{:else if m === 'right'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<line x1="2.5" y1="3" x2="15.5" y2="3" stroke="currentColor" stroke-width="1.2" />
|
||||
<rect x="9.5" y="6" width="6" height="6" rx="1" fill="currentColor" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
// Small popover to enter/edit a hyperlink URL. The parent owns `open` and decides how
|
||||
// to apply the URL to the selection (insert vs. set vs. extend); this just collects it.
|
||||
let {
|
||||
@@ -40,7 +41,7 @@
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="link-dialog" role="dialog" aria-label="Insert link">
|
||||
<div class="link-dialog" role="dialog" aria-label={t().link.dialogLabel}>
|
||||
<input
|
||||
bind:this={input}
|
||||
bind:value={url}
|
||||
@@ -52,11 +53,11 @@
|
||||
/>
|
||||
<div class="link-actions">
|
||||
{#if canRemove}
|
||||
<button class="link-remove" onclick={onRemove}>Remove</button>
|
||||
<button class="link-remove" onclick={onRemove}>{t().common.remove}</button>
|
||||
{/if}
|
||||
<span class="link-spacer"></span>
|
||||
<button class="link-cancel" onclick={onClose}>Cancel</button>
|
||||
<button class="link-apply" onclick={apply} disabled={!url.trim()}>Apply</button>
|
||||
<button class="link-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="link-apply" onclick={apply} disabled={!url.trim()}>{t().common.apply}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { CHAR_CATEGORIES, type SpecialChar } from '../utils/specialChars';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
// Category tab label by its English name, with the name itself as fallback.
|
||||
function categoryLabel(name: string): string {
|
||||
return (t().special.categories as Record<string, string>)[name] ?? name;
|
||||
}
|
||||
|
||||
let {
|
||||
editor,
|
||||
@@ -121,26 +127,26 @@
|
||||
</script>
|
||||
|
||||
<div class="char-picker" use:charPickerClickOutside>
|
||||
<button class="char-trigger" onclick={openPicker} title="Insert special character" aria-pressed={open}>
|
||||
<button class="char-trigger" onclick={openPicker} title={t().special.insertChar} aria-pressed={open}>
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<text x="8" y="12.5" font-size="14" font-weight="700" font-family="var(--font-serif, serif)" fill="currentColor" text-anchor="middle">Ω</text>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div class="char-dropdown" role="dialog" aria-label="Insert special character" tabindex="-1" onkeydown={onKeydown}>
|
||||
<div class="char-dropdown" role="dialog" aria-label={t().special.insertChar} tabindex="-1" onkeydown={onKeydown}>
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
class="char-search"
|
||||
bind:value={query}
|
||||
placeholder="Search by name or code…"
|
||||
aria-label="Search special characters"
|
||||
placeholder={t().special.searchPlaceholder}
|
||||
aria-label={t().special.searchAria}
|
||||
autofocus
|
||||
/>
|
||||
|
||||
{#if !query && recentChars.length > 0}
|
||||
<div class="char-section-label">Recent</div>
|
||||
<div class="char-section-label">{t().special.recent}</div>
|
||||
<div class="char-grid">
|
||||
{#each recentChars as c (c)}
|
||||
<button class="char-cell" title={titleOf(c)} onclick={() => pick(c)}>{c}</button>
|
||||
@@ -157,7 +163,7 @@
|
||||
role="tab"
|
||||
aria-selected={i === activeIndex}
|
||||
onclick={() => (activeIndex = i)}
|
||||
>{cat.name}</button>
|
||||
>{categoryLabel(cat.name)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -169,7 +175,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="char-empty">No matching characters</div>
|
||||
<div class="char-empty">{t().special.noMatch}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
let {
|
||||
top,
|
||||
left,
|
||||
@@ -42,18 +43,18 @@
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
aria-label="Spelling suggestions"
|
||||
aria-label={t().spell.suggestions}
|
||||
>
|
||||
{#if suggestions.length}
|
||||
{#each suggestions as s}
|
||||
<button class="sm-item sm-suggest" role="menuitem" onclick={() => onReplace(s)}>{s}</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="sm-empty">No suggestions</span>
|
||||
<span class="sm-empty">{t().spell.noSuggestions}</span>
|
||||
{/if}
|
||||
<span class="sm-sep"></span>
|
||||
<button class="sm-item" role="menuitem" onclick={onAdd}>Add to Dictionary</button>
|
||||
<button class="sm-item" role="menuitem" onclick={onIgnore}>Ignore All</button>
|
||||
<button class="sm-item" role="menuitem" onclick={onAdd}>{t().spell.addToDictionary}</button>
|
||||
<button class="sm-item" role="menuitem" onclick={onIgnore}>{t().spell.ignoreAll}</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { activeBorderPresets, type BorderPreset } from '../editor/extensions/tableCellBorders';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let { editor, tick }: { editor: Editor | null; tick: number } = $props();
|
||||
|
||||
@@ -24,17 +25,17 @@
|
||||
h: 'M1 9H17', v: 'M9 1V17',
|
||||
};
|
||||
const ALL_SEGS: Seg[] = ['top', 'right', 'bottom', 'left', 'h', 'v'];
|
||||
const PRESETS: { id: BorderPreset | 'none'; title: string; on: Seg[] }[] = [
|
||||
{ id: 'all', title: 'All borders', on: ['top', 'right', 'bottom', 'left', 'h', 'v'] },
|
||||
{ id: 'outer', title: 'Outside borders', on: ['top', 'right', 'bottom', 'left'] },
|
||||
{ id: 'inner', title: 'Inside borders', on: ['h', 'v'] },
|
||||
{ id: 'innerH', title: 'Inside horizontal border', on: ['h'] },
|
||||
{ id: 'innerV', title: 'Inside vertical border', on: ['v'] },
|
||||
{ id: 'top', title: 'Top border', on: ['top'] },
|
||||
{ id: 'bottom', title: 'Bottom border', on: ['bottom'] },
|
||||
{ id: 'left', title: 'Left border', on: ['left'] },
|
||||
{ id: 'right', title: 'Right border', on: ['right'] },
|
||||
{ id: 'none', title: 'No border', on: [] },
|
||||
const PRESETS: { id: BorderPreset | 'none'; on: Seg[] }[] = [
|
||||
{ id: 'all', on: ['top', 'right', 'bottom', 'left', 'h', 'v'] },
|
||||
{ id: 'outer', on: ['top', 'right', 'bottom', 'left'] },
|
||||
{ id: 'inner', on: ['h', 'v'] },
|
||||
{ id: 'innerH', on: ['h'] },
|
||||
{ id: 'innerV', on: ['v'] },
|
||||
{ id: 'top', on: ['top'] },
|
||||
{ id: 'bottom', on: ['bottom'] },
|
||||
{ id: 'left', on: ['left'] },
|
||||
{ id: 'right', on: ['right'] },
|
||||
{ id: 'none', on: [] },
|
||||
];
|
||||
|
||||
// Word-like button states, re-read per transaction (tick) and pen change: a preset
|
||||
@@ -65,8 +66,8 @@
|
||||
<button
|
||||
class="bp-trigger"
|
||||
class:active={open}
|
||||
title="Borders"
|
||||
aria-label="Borders"
|
||||
title={t().borders.title}
|
||||
aria-label={t().borders.title}
|
||||
aria-expanded={open}
|
||||
onclick={() => (open = !open)}
|
||||
>
|
||||
@@ -82,14 +83,14 @@
|
||||
|
||||
{#if open}
|
||||
<div class="bp-dropdown">
|
||||
<div class="bp-title">Borders</div>
|
||||
<div class="bp-title">{t().borders.title}</div>
|
||||
<div class="bp-presets">
|
||||
{#each PRESETS as preset}
|
||||
<button
|
||||
class="bp-preset"
|
||||
class:active={!!active?.[preset.id]}
|
||||
title={preset.title}
|
||||
aria-label={preset.title}
|
||||
title={t().borders[preset.id]}
|
||||
aria-label={t().borders[preset.id]}
|
||||
aria-pressed={!!active?.[preset.id]}
|
||||
onclick={() => apply(preset.id)}
|
||||
>
|
||||
@@ -118,14 +119,14 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="bp-title">Line width</div>
|
||||
<div class="bp-title">{t().borders.lineWidth}</div>
|
||||
<div class="bp-widths">
|
||||
{#each WIDTHS as w}
|
||||
<button
|
||||
class="bp-width"
|
||||
class:active={widthPt === w}
|
||||
title="{w}pt"
|
||||
aria-label="{w}pt"
|
||||
title={t().borders.pt(w)}
|
||||
aria-label={t().borders.pt(w)}
|
||||
aria-pressed={widthPt === w}
|
||||
onclick={() => (widthPt = w)}
|
||||
>
|
||||
@@ -134,7 +135,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="bp-title">Line color</div>
|
||||
<div class="bp-title">{t().borders.lineColor}</div>
|
||||
<div class="bp-colors">
|
||||
{#each COLORS as row}
|
||||
<div class="bp-color-row">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
editor,
|
||||
@@ -84,7 +85,7 @@
|
||||
</script>
|
||||
|
||||
<div class="table-picker" use:tablePickerClickOutside>
|
||||
<button class="table-trigger" onclick={openPicker} title="Insert table" aria-pressed={open}>
|
||||
<button class="table-trigger" onclick={openPicker} title={t().table.insertTable} aria-pressed={open}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="1.5" y="1.5" width="13" height="13" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="1.5" y1="5.5" x2="14.5" y2="5.5" stroke="currentColor" stroke-width="1"/>
|
||||
@@ -98,9 +99,9 @@
|
||||
<div class="table-dropdown">
|
||||
<div class="table-dim-label">
|
||||
{#if hoverRows > 0 && hoverCols > 0}
|
||||
{hoverCols} × {hoverRows}
|
||||
{t().table.dimensions(hoverCols, hoverRows)}
|
||||
{:else}
|
||||
Insert table
|
||||
{t().table.insertTable}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -109,7 +110,7 @@
|
||||
class="table-grid"
|
||||
role="grid"
|
||||
tabindex="-1"
|
||||
aria-label="Select table size"
|
||||
aria-label={t().table.selectSize}
|
||||
onmouseleave={() => { hoverRows = 0; hoverCols = 0; }}
|
||||
>
|
||||
{#each ROW_INDICES as r}
|
||||
@@ -119,7 +120,7 @@
|
||||
class="table-grid-cell"
|
||||
class:hot={r <= hoverRows && c <= hoverCols}
|
||||
role="gridcell"
|
||||
aria-label={`${c} by ${r}`}
|
||||
aria-label={t().table.byDimension(c, r)}
|
||||
onmouseover={() => { hoverRows = r; hoverCols = c; }}
|
||||
onfocus={() => { hoverRows = r; hoverCols = c; }}
|
||||
onclick={() => insert(r, c)}
|
||||
@@ -131,26 +132,26 @@
|
||||
|
||||
<div class="table-manual">
|
||||
<label class="table-manual-field">
|
||||
<span>Rows</span>
|
||||
<span>{t().table.rows}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
bind:value={rowsInput}
|
||||
onkeydown={onInputKeydown}
|
||||
aria-label="Number of rows"
|
||||
aria-label={t().table.numberOfRows}
|
||||
/>
|
||||
</label>
|
||||
<label class="table-manual-field">
|
||||
<span>Cols</span>
|
||||
<span>{t().table.cols}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
bind:value={colsInput}
|
||||
onkeydown={onInputKeydown}
|
||||
aria-label="Number of columns"
|
||||
aria-label={t().table.numberOfColumns}
|
||||
/>
|
||||
</label>
|
||||
<button class="table-insert-btn" onclick={insertFromInputs}>Insert</button>
|
||||
<button class="table-insert-btn" onclick={insertFromInputs}>{t().table.insert}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
// Word/LibreOffice "Split Cells…" popover: collects column + row counts. The parent
|
||||
// owns `open` and runs the splitCellInto command against the saved cell selection
|
||||
// (focus moves to the inputs, like LinkDialog/TablePicker).
|
||||
@@ -55,11 +56,11 @@
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
aria-label="Split cells"
|
||||
aria-label={t().table.splitDialogLabel}
|
||||
>
|
||||
<div class="split-fields">
|
||||
<label>
|
||||
<span>Columns</span>
|
||||
<span>{t().table.columns}</span>
|
||||
<input
|
||||
bind:this={firstField}
|
||||
bind:value={colsInput}
|
||||
@@ -70,7 +71,7 @@
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Rows</span>
|
||||
<span>{t().table.rowsField}</span>
|
||||
<input
|
||||
bind:value={rowsInput}
|
||||
type="number"
|
||||
@@ -82,8 +83,8 @@
|
||||
</div>
|
||||
<div class="split-actions">
|
||||
<span class="split-spacer"></span>
|
||||
<button class="split-cancel" onclick={onClose}>Cancel</button>
|
||||
<button class="split-apply" onclick={apply}>Split</button>
|
||||
<button class="split-cancel" onclick={onClose}>{t().common.cancel}</button>
|
||||
<button class="split-apply" onclick={apply}>{t().table.split}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { isHeaderRowStyled } from '../editor/extensions/tableHeaderRow';
|
||||
import ColorPicker from './ColorPicker.svelte';
|
||||
import TableBorderPicker from './TableBorderPicker.svelte';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
editor,
|
||||
@@ -70,13 +71,13 @@
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="toolbar"
|
||||
tabindex="-1"
|
||||
aria-label="Table editing"
|
||||
aria-label={t().table.editing}
|
||||
onmousedown={(e) => { if (!(e.target as HTMLElement).closest('.color-picker')) e.preventDefault(); }}
|
||||
>
|
||||
<button
|
||||
class="tt-btn"
|
||||
title="Insert row above"
|
||||
aria-label="Insert row above"
|
||||
title={t().table.insertRowAbove}
|
||||
aria-label={t().table.insertRowAbove}
|
||||
onclick={() => run((c) => c.addRowBefore())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -88,8 +89,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn"
|
||||
title="Insert row below"
|
||||
aria-label="Insert row below"
|
||||
title={t().table.insertRowBelow}
|
||||
aria-label={t().table.insertRowBelow}
|
||||
onclick={() => run((c) => c.addRowAfter())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -101,8 +102,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn"
|
||||
title="Insert column left"
|
||||
aria-label="Insert column left"
|
||||
title={t().table.insertColumnLeft}
|
||||
aria-label={t().table.insertColumnLeft}
|
||||
onclick={() => run((c) => c.addColumnBefore())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -114,8 +115,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn"
|
||||
title="Insert column right"
|
||||
aria-label="Insert column right"
|
||||
title={t().table.insertColumnRight}
|
||||
aria-label={t().table.insertColumnRight}
|
||||
onclick={() => run((c) => c.addColumnAfter())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -129,8 +130,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn tt-danger"
|
||||
title="Delete row"
|
||||
aria-label="Delete row"
|
||||
title={t().table.deleteRow}
|
||||
aria-label={t().table.deleteRow}
|
||||
onclick={() => run((c) => c.deleteRow())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -142,8 +143,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn tt-danger"
|
||||
title="Delete column"
|
||||
aria-label="Delete column"
|
||||
title={t().table.deleteColumn}
|
||||
aria-label={t().table.deleteColumn}
|
||||
onclick={() => run((c) => c.deleteColumn())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -155,8 +156,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn tt-danger"
|
||||
title="Delete table"
|
||||
aria-label="Delete table"
|
||||
title={t().table.deleteTable}
|
||||
aria-label={t().table.deleteTable}
|
||||
onclick={() => run((c) => c.deleteTable())}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -168,8 +169,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn"
|
||||
title="Merge cells"
|
||||
aria-label="Merge cells"
|
||||
title={t().table.mergeCells}
|
||||
aria-label={t().table.mergeCells}
|
||||
disabled={!canMerge}
|
||||
onclick={() => run((c) => c.mergeCells())}
|
||||
>
|
||||
@@ -182,8 +183,8 @@
|
||||
|
||||
<button
|
||||
class="tt-btn"
|
||||
title="Split cells…"
|
||||
aria-label="Split cells"
|
||||
title={t().table.splitCells}
|
||||
aria-label={t().table.splitCellsAria}
|
||||
onclick={() => onSplit()}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
@@ -199,9 +200,9 @@
|
||||
{editor}
|
||||
currentColor={currentCellColor}
|
||||
defaultColor="#D9D9D9"
|
||||
title="Cell shading"
|
||||
chevronTitle="Choose cell color"
|
||||
clearLabel="No fill"
|
||||
title={t().table.cellShading}
|
||||
chevronTitle={t().table.chooseCellColor}
|
||||
clearLabel={t().table.noFill}
|
||||
onApply={(c) => applyCellColor(c)}
|
||||
onClear={() => clearCellColor()}
|
||||
>
|
||||
@@ -221,8 +222,8 @@
|
||||
<button
|
||||
class="tt-btn"
|
||||
class:active={isHeaderRow}
|
||||
title="Header row (bold + shading)"
|
||||
aria-label="Header row"
|
||||
title={t().table.headerRow}
|
||||
aria-label={t().table.headerRowAria}
|
||||
aria-pressed={isHeaderRow}
|
||||
onclick={() => run((c) => c.toggleHeaderRowStyle())}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { WrapMode } from '../editor/extensions/image';
|
||||
import type { ShapeKind, TextBoxAttrs } from '../editor/extensions/textBox';
|
||||
import ColorPicker from './ColorPicker.svelte';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
editor,
|
||||
@@ -31,18 +32,20 @@
|
||||
editor?.chain().focus().setTextBoxAttrs(attrs).run();
|
||||
}
|
||||
|
||||
const wrapModes: { mode: WrapMode; title: string }[] = [
|
||||
{ mode: 'inline', title: 'In line with text' },
|
||||
{ mode: 'left', title: 'Wrap text — box left' },
|
||||
{ mode: 'right', title: 'Wrap text — box right' },
|
||||
{ mode: 'topBottom', title: 'Top and bottom' },
|
||||
];
|
||||
const wrapModes: WrapMode[] = ['inline', 'left', 'right', 'topBottom'];
|
||||
function wrapTitle(m: WrapMode): string {
|
||||
return m === 'inline' ? t().textBox.wrapInline
|
||||
: m === 'left' ? t().textBox.wrapLeft
|
||||
: m === 'right' ? t().textBox.wrapRight
|
||||
: t().textBox.wrapTopBottom;
|
||||
}
|
||||
|
||||
const shapes: { kind: ShapeKind; title: string }[] = [
|
||||
{ kind: 'textbox', title: 'Rectangle' },
|
||||
{ kind: 'roundRect', title: 'Rounded rectangle' },
|
||||
{ kind: 'ellipse', title: 'Ellipse' },
|
||||
];
|
||||
const shapes: ShapeKind[] = ['textbox', 'roundRect', 'ellipse'];
|
||||
function shapeTitle(k: ShapeKind): string {
|
||||
return k === 'textbox' ? t().textBox.shapeRect
|
||||
: k === 'roundRect' ? t().textBox.shapeRoundRect
|
||||
: t().textBox.shapeEllipse;
|
||||
}
|
||||
|
||||
const strokeWidths = [0.5, 1, 2.25];
|
||||
</script>
|
||||
@@ -52,7 +55,7 @@
|
||||
style="top: {top}px; left: {left}px;"
|
||||
role="toolbar"
|
||||
tabindex="-1"
|
||||
aria-label="Text box options"
|
||||
aria-label={t().textBox.toolbar}
|
||||
onmousedown={(e) => {
|
||||
// Keep the box selection/caret; buttons work via click.
|
||||
if ((e.target as HTMLElement).closest('select') === null) e.preventDefault();
|
||||
@@ -61,13 +64,13 @@
|
||||
{#each wrapModes as m}
|
||||
<button
|
||||
class="tb-btn"
|
||||
class:active={wrap === m.mode}
|
||||
title={m.title}
|
||||
aria-label={m.title}
|
||||
aria-pressed={wrap === m.mode}
|
||||
onclick={() => set({ wrap: m.mode })}
|
||||
class:active={wrap === m}
|
||||
title={wrapTitle(m)}
|
||||
aria-label={wrapTitle(m)}
|
||||
aria-pressed={wrap === m}
|
||||
onclick={() => set({ wrap: m })}
|
||||
>
|
||||
{#if m.mode === 'inline'}
|
||||
{#if m === 'inline'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<line x1="2.5" y1="3" x2="15.5" y2="3" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2.5" y1="12" x2="5.5" y2="12" stroke="currentColor" stroke-width="1.2" />
|
||||
@@ -75,7 +78,7 @@
|
||||
<line x1="12.5" y1="12" x2="15.5" y2="12" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2.5" y1="15" x2="15.5" y2="15" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{:else if m.mode === 'left'}
|
||||
{:else if m === 'left'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<line x1="2.5" y1="3" x2="15.5" y2="3" stroke="currentColor" stroke-width="1.2" />
|
||||
<rect x="2.5" y="6" width="6" height="6" rx="1" fill="currentColor" />
|
||||
@@ -83,7 +86,7 @@
|
||||
<line x1="10" y1="10" x2="15.5" y2="10" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2.5" y1="15" x2="15.5" y2="15" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
{:else if m.mode === 'right'}
|
||||
{:else if m === 'right'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<line x1="2.5" y1="3" x2="15.5" y2="3" stroke="currentColor" stroke-width="1.2" />
|
||||
<rect x="9.5" y="6" width="6" height="6" rx="1" fill="currentColor" />
|
||||
@@ -106,17 +109,17 @@
|
||||
{#each shapes as s}
|
||||
<button
|
||||
class="tb-btn"
|
||||
class:active={shapeKind === s.kind}
|
||||
title={s.title}
|
||||
aria-label={s.title}
|
||||
aria-pressed={shapeKind === s.kind}
|
||||
onclick={() => set({ shapeKind: s.kind })}
|
||||
class:active={shapeKind === s}
|
||||
title={shapeTitle(s)}
|
||||
aria-label={shapeTitle(s)}
|
||||
aria-pressed={shapeKind === s}
|
||||
onclick={() => set({ shapeKind: s })}
|
||||
>
|
||||
{#if s.kind === 'textbox'}
|
||||
{#if s === 'textbox'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<rect x="2.5" y="4.5" width="13" height="9" stroke="currentColor" stroke-width="1.3" />
|
||||
</svg>
|
||||
{:else if s.kind === 'roundRect'}
|
||||
{:else if s === 'roundRect'}
|
||||
<svg width="22" height="22" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<rect x="2.5" y="4.5" width="13" height="9" rx="3" stroke="currentColor" stroke-width="1.3" />
|
||||
</svg>
|
||||
@@ -135,9 +138,9 @@
|
||||
bind:open={fillOpen}
|
||||
currentColor={fillColor}
|
||||
defaultColor="#FFFFFF"
|
||||
title="Fill color"
|
||||
chevronTitle="Choose fill color"
|
||||
clearLabel="No fill"
|
||||
title={t().textBox.fillColor}
|
||||
chevronTitle={t().textBox.chooseFillColor}
|
||||
clearLabel={t().textBox.noFill}
|
||||
onOpen={() => (strokeOpen = false)}
|
||||
onApply={(c) => set({ fillColor: c })}
|
||||
onClear={() => set({ fillColor: null })}
|
||||
@@ -156,9 +159,9 @@
|
||||
bind:open={strokeOpen}
|
||||
currentColor={strokeColor}
|
||||
defaultColor="#000000"
|
||||
title="Border color"
|
||||
chevronTitle="Choose border color"
|
||||
clearLabel="No border"
|
||||
title={t().textBox.borderColor}
|
||||
chevronTitle={t().textBox.chooseBorderColor}
|
||||
clearLabel={t().textBox.noBorder}
|
||||
onOpen={() => (fillOpen = false)}
|
||||
onApply={(c) => set({ strokeColor: c })}
|
||||
onClear={() => set({ strokeColor: null })}
|
||||
@@ -173,16 +176,16 @@
|
||||
|
||||
<select
|
||||
class="tb-width"
|
||||
title="Border width"
|
||||
aria-label="Border width"
|
||||
title={t().textBox.borderWidth}
|
||||
aria-label={t().textBox.borderWidth}
|
||||
value={String(strokeWidthPt)}
|
||||
onchange={(e) => set({ strokeWidthPt: parseFloat((e.target as HTMLSelectElement).value) })}
|
||||
>
|
||||
{#each strokeWidths as w}
|
||||
<option value={String(w)}>{w} pt</option>
|
||||
<option value={String(w)}>{t().textBox.pt(w)}</option>
|
||||
{/each}
|
||||
{#if !strokeWidths.includes(strokeWidthPt)}
|
||||
<option value={String(strokeWidthPt)}>{strokeWidthPt} pt</option>
|
||||
<option value={String(strokeWidthPt)}>{t().textBox.pt(strokeWidthPt)}</option>
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,147 @@ const de: Messages = {
|
||||
linkNotInHf: 'Links sind in Kopf-/Fußzeilen nicht verfügbar',
|
||||
clearFormatting: 'Formatierung entfernen',
|
||||
},
|
||||
image: {
|
||||
wrapToolbar: 'Bildumbruch',
|
||||
wrapInline: 'Mit Text in Zeile',
|
||||
wrapLeft: 'Textumbruch — Bild links',
|
||||
wrapRight: 'Textumbruch — Bild rechts',
|
||||
wrapTopBottom: 'Oben und unten',
|
||||
},
|
||||
textBox: {
|
||||
toolbar: 'Textfeld-Optionen',
|
||||
wrapInline: 'Mit Text in Zeile',
|
||||
wrapLeft: 'Textumbruch — Feld links',
|
||||
wrapRight: 'Textumbruch — Feld rechts',
|
||||
wrapTopBottom: 'Oben und unten',
|
||||
shapeRect: 'Rechteck',
|
||||
shapeRoundRect: 'Abgerundetes Rechteck',
|
||||
shapeEllipse: 'Ellipse',
|
||||
fillColor: 'Füllfarbe',
|
||||
chooseFillColor: 'Füllfarbe wählen',
|
||||
noFill: 'Keine Füllung',
|
||||
borderColor: 'Rahmenfarbe',
|
||||
chooseBorderColor: 'Rahmenfarbe wählen',
|
||||
noBorder: 'Kein Rahmen',
|
||||
borderWidth: 'Rahmenbreite',
|
||||
pt: (w) => `${w} pt`,
|
||||
},
|
||||
link: {
|
||||
dialogLabel: 'Link einfügen',
|
||||
},
|
||||
color: {
|
||||
automatic: 'Automatisch',
|
||||
moreColors: 'Weitere Farben…',
|
||||
pickFromPage: 'Farbe von der Seite auswählen',
|
||||
chooseColor: 'Farbe wählen',
|
||||
},
|
||||
table: {
|
||||
editing: 'Tabelle bearbeiten',
|
||||
insertRowAbove: 'Zeile oberhalb einfügen',
|
||||
insertRowBelow: 'Zeile unterhalb einfügen',
|
||||
insertColumnLeft: 'Spalte links einfügen',
|
||||
insertColumnRight: 'Spalte rechts einfügen',
|
||||
deleteRow: 'Zeile löschen',
|
||||
deleteColumn: 'Spalte löschen',
|
||||
deleteTable: 'Tabelle löschen',
|
||||
mergeCells: 'Zellen verbinden',
|
||||
splitCells: 'Zellen teilen…',
|
||||
splitCellsAria: 'Zellen teilen',
|
||||
cellShading: 'Zellenschattierung',
|
||||
chooseCellColor: 'Zellenfarbe wählen',
|
||||
noFill: 'Keine Füllung',
|
||||
headerRow: 'Kopfzeile (fett + Schattierung)',
|
||||
headerRowAria: 'Kopfzeile',
|
||||
insertTable: 'Tabelle einfügen',
|
||||
selectSize: 'Tabellengröße wählen',
|
||||
dimensions: (cols, rows) => `${cols} × ${rows}`,
|
||||
byDimension: (c, r) => `${c} mal ${r}`,
|
||||
rows: 'Zeilen',
|
||||
cols: 'Spalten',
|
||||
numberOfRows: 'Anzahl der Zeilen',
|
||||
numberOfColumns: 'Anzahl der Spalten',
|
||||
insert: 'Einfügen',
|
||||
splitDialogLabel: 'Zellen teilen',
|
||||
columns: 'Spalten',
|
||||
rowsField: 'Zeilen',
|
||||
split: 'Teilen',
|
||||
},
|
||||
borders: {
|
||||
title: 'Rahmen',
|
||||
all: 'Alle Rahmen',
|
||||
outer: 'Außenrahmen',
|
||||
inner: 'Innenrahmen',
|
||||
innerH: 'Innerer waagerechter Rahmen',
|
||||
innerV: 'Innerer senkrechter Rahmen',
|
||||
top: 'Oberer Rahmen',
|
||||
bottom: 'Unterer Rahmen',
|
||||
left: 'Linker Rahmen',
|
||||
right: 'Rechter Rahmen',
|
||||
none: 'Kein Rahmen',
|
||||
lineWidth: 'Linienbreite',
|
||||
lineColor: 'Linienfarbe',
|
||||
pt: (w) => `${w}pt`,
|
||||
},
|
||||
spell: {
|
||||
suggestions: 'Rechtschreibvorschläge',
|
||||
noSuggestions: 'Keine Vorschläge',
|
||||
addToDictionary: 'Zum Wörterbuch hinzufügen',
|
||||
ignoreAll: 'Alle ignorieren',
|
||||
},
|
||||
about: {
|
||||
label: 'Über EdenText',
|
||||
tagline: 'Ein privater Editor für OpenDocument-Dateien — läuft vollständig im Browser.',
|
||||
version: (v) => `Version ${v}`,
|
||||
viewOnGitHub: 'Auf GitHub ansehen',
|
||||
},
|
||||
findReplace: {
|
||||
dialogLabel: 'Suchen und ersetzen',
|
||||
hideReplace: 'Ersetzen ausblenden',
|
||||
showReplace: 'Ersetzen anzeigen',
|
||||
toggleReplace: 'Ersetzen umschalten',
|
||||
findPlaceholder: 'Suchen',
|
||||
noResults: 'Keine Treffer',
|
||||
matchCase: 'Groß-/Kleinschreibung beachten',
|
||||
wholeWord: 'Ganzes Wort',
|
||||
previous: 'Zurück',
|
||||
previousAria: 'Vorheriger Treffer',
|
||||
next: 'Weiter',
|
||||
nextAria: 'Nächster Treffer',
|
||||
replacePlaceholder: 'Ersetzen',
|
||||
replaceCurrent: 'Aktuellen Treffer ersetzen',
|
||||
replaceAllTitle: 'Alle Treffer ersetzen',
|
||||
replace: 'Ersetzen',
|
||||
all: 'Alle',
|
||||
},
|
||||
special: {
|
||||
insertChar: 'Sonderzeichen einfügen',
|
||||
searchPlaceholder: 'Nach Name oder Code suchen…',
|
||||
searchAria: 'Sonderzeichen suchen',
|
||||
recent: 'Zuletzt',
|
||||
noMatch: 'Keine passenden Zeichen',
|
||||
categories: {
|
||||
Punctuation: 'Interpunktion',
|
||||
Currency: 'Währung',
|
||||
Math: 'Mathematik',
|
||||
Arrows: 'Pfeile',
|
||||
Greek: 'Griechisch',
|
||||
Latin: 'Lateinisch',
|
||||
Symbols: 'Symbole',
|
||||
},
|
||||
},
|
||||
hf: {
|
||||
addHeaderHint: 'Zum Hinzufügen einer Kopfzeile doppelklicken',
|
||||
addFooterHint: 'Zum Hinzufügen einer Fußzeile doppelklicken',
|
||||
headerPlaceholder: 'Kopfzeile…',
|
||||
footerPlaceholder: 'Fußzeile…',
|
||||
insert: 'Einfügen',
|
||||
pageNumber: 'Seitenzahl',
|
||||
pageNumberTitle: 'Aktuelle Seitenzahl einfügen',
|
||||
pageCount: 'Seitenanzahl',
|
||||
pageCountTitle: 'Gesamtseitenanzahl einfügen',
|
||||
done: 'Fertig',
|
||||
doneTitle: 'Bearbeiten der Kopf-/Fußzeile beenden',
|
||||
},
|
||||
};
|
||||
|
||||
export default de;
|
||||
@@ -122,6 +122,147 @@ const en = {
|
||||
linkNotInHf: 'Links are not available in headers/footers',
|
||||
clearFormatting: 'Clear formatting',
|
||||
},
|
||||
image: {
|
||||
wrapToolbar: 'Image text wrap',
|
||||
wrapInline: 'In line with text',
|
||||
wrapLeft: 'Wrap text — image left',
|
||||
wrapRight: 'Wrap text — image right',
|
||||
wrapTopBottom: 'Top and bottom',
|
||||
},
|
||||
textBox: {
|
||||
toolbar: 'Text box options',
|
||||
wrapInline: 'In line with text',
|
||||
wrapLeft: 'Wrap text — box left',
|
||||
wrapRight: 'Wrap text — box right',
|
||||
wrapTopBottom: 'Top and bottom',
|
||||
shapeRect: 'Rectangle',
|
||||
shapeRoundRect: 'Rounded rectangle',
|
||||
shapeEllipse: 'Ellipse',
|
||||
fillColor: 'Fill color',
|
||||
chooseFillColor: 'Choose fill color',
|
||||
noFill: 'No fill',
|
||||
borderColor: 'Border color',
|
||||
chooseBorderColor: 'Choose border color',
|
||||
noBorder: 'No border',
|
||||
borderWidth: 'Border width',
|
||||
pt: (w: number) => `${w} pt`,
|
||||
},
|
||||
link: {
|
||||
dialogLabel: 'Insert link',
|
||||
},
|
||||
color: {
|
||||
automatic: 'Automatic',
|
||||
moreColors: 'More colors…',
|
||||
pickFromPage: 'Pick a color from the page',
|
||||
chooseColor: 'Choose color',
|
||||
},
|
||||
table: {
|
||||
editing: 'Table editing',
|
||||
insertRowAbove: 'Insert row above',
|
||||
insertRowBelow: 'Insert row below',
|
||||
insertColumnLeft: 'Insert column left',
|
||||
insertColumnRight: 'Insert column right',
|
||||
deleteRow: 'Delete row',
|
||||
deleteColumn: 'Delete column',
|
||||
deleteTable: 'Delete table',
|
||||
mergeCells: 'Merge cells',
|
||||
splitCells: 'Split cells…',
|
||||
splitCellsAria: 'Split cells',
|
||||
cellShading: 'Cell shading',
|
||||
chooseCellColor: 'Choose cell color',
|
||||
noFill: 'No fill',
|
||||
headerRow: 'Header row (bold + shading)',
|
||||
headerRowAria: 'Header row',
|
||||
insertTable: 'Insert table',
|
||||
selectSize: 'Select table size',
|
||||
dimensions: (cols: number, rows: number) => `${cols} × ${rows}`,
|
||||
byDimension: (c: number, r: number) => `${c} by ${r}`,
|
||||
rows: 'Rows',
|
||||
cols: 'Cols',
|
||||
numberOfRows: 'Number of rows',
|
||||
numberOfColumns: 'Number of columns',
|
||||
insert: 'Insert',
|
||||
splitDialogLabel: 'Split cells',
|
||||
columns: 'Columns',
|
||||
rowsField: 'Rows',
|
||||
split: 'Split',
|
||||
},
|
||||
borders: {
|
||||
title: 'Borders',
|
||||
all: 'All borders',
|
||||
outer: 'Outside borders',
|
||||
inner: 'Inside borders',
|
||||
innerH: 'Inside horizontal border',
|
||||
innerV: 'Inside vertical border',
|
||||
top: 'Top border',
|
||||
bottom: 'Bottom border',
|
||||
left: 'Left border',
|
||||
right: 'Right border',
|
||||
none: 'No border',
|
||||
lineWidth: 'Line width',
|
||||
lineColor: 'Line color',
|
||||
pt: (w: number) => `${w}pt`,
|
||||
},
|
||||
spell: {
|
||||
suggestions: 'Spelling suggestions',
|
||||
noSuggestions: 'No suggestions',
|
||||
addToDictionary: 'Add to Dictionary',
|
||||
ignoreAll: 'Ignore All',
|
||||
},
|
||||
about: {
|
||||
label: 'About EdenText',
|
||||
tagline: 'A private editor for OpenDocument files — runs entirely in your browser.',
|
||||
version: (v: string) => `Version ${v}`,
|
||||
viewOnGitHub: 'View on GitHub',
|
||||
},
|
||||
findReplace: {
|
||||
dialogLabel: 'Find and replace',
|
||||
hideReplace: 'Hide replace',
|
||||
showReplace: 'Show replace',
|
||||
toggleReplace: 'Toggle replace',
|
||||
findPlaceholder: 'Find',
|
||||
noResults: 'No results',
|
||||
matchCase: 'Match case',
|
||||
wholeWord: 'Whole word',
|
||||
previous: 'Previous',
|
||||
previousAria: 'Previous match',
|
||||
next: 'Next',
|
||||
nextAria: 'Next match',
|
||||
replacePlaceholder: 'Replace',
|
||||
replaceCurrent: 'Replace current match',
|
||||
replaceAllTitle: 'Replace all matches',
|
||||
replace: 'Replace',
|
||||
all: 'All',
|
||||
},
|
||||
special: {
|
||||
insertChar: 'Insert special character',
|
||||
searchPlaceholder: 'Search by name or code…',
|
||||
searchAria: 'Search special characters',
|
||||
recent: 'Recent',
|
||||
noMatch: 'No matching characters',
|
||||
categories: {
|
||||
Punctuation: 'Punctuation',
|
||||
Currency: 'Currency',
|
||||
Math: 'Math',
|
||||
Arrows: 'Arrows',
|
||||
Greek: 'Greek',
|
||||
Latin: 'Latin',
|
||||
Symbols: 'Symbols',
|
||||
},
|
||||
},
|
||||
hf: {
|
||||
addHeaderHint: 'Double-click to add a header',
|
||||
addFooterHint: 'Double-click to add a footer',
|
||||
headerPlaceholder: 'Header…',
|
||||
footerPlaceholder: 'Footer…',
|
||||
insert: 'Insert',
|
||||
pageNumber: 'Page number',
|
||||
pageNumberTitle: 'Insert the current page number',
|
||||
pageCount: 'Page count',
|
||||
pageCountTitle: 'Insert the total page count',
|
||||
done: 'Done',
|
||||
doneTitle: 'Finish editing the header/footer',
|
||||
},
|
||||
};
|
||||
|
||||
export type Messages = typeof en;
|
||||
|
||||
Reference in new issue
Block a user