@open-slide/core 1.0.4 → 1.0.6
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.
- package/dist/{build-DqfKmw9h.js → build-4wOJF1l4.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-DweCbRkQ.d.ts → config-D2y1AXaN.d.ts} +3 -0
- package/dist/{config-CN7J0RDO.js → config-evLWCV1-.js} +378 -222
- package/dist/{dev-jWxtWHAG.js → dev-BUr0S-Ij.js} +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +1189 -0
- package/dist/{preview-CSA05Gfm.js → preview-DP_gIphz.js} +1 -1
- package/dist/types-BVvl_xup.d.ts +314 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +1 -1
- package/package.json +7 -1
- package/src/app/app.tsx +6 -2
- package/src/app/components/asset-view.tsx +87 -64
- package/src/app/components/click-nav-zones.tsx +4 -2
- package/src/app/components/inspector/comment-widget.tsx +9 -7
- package/src/app/components/inspector/inspect-overlay.tsx +79 -17
- package/src/app/components/inspector/inspector-panel.tsx +68 -39
- package/src/app/components/inspector/inspector-provider.tsx +185 -58
- package/src/app/components/inspector/save-bar.tsx +6 -5
- package/src/app/components/panel/save-card.tsx +12 -9
- package/src/app/components/pdf-progress-toast.tsx +11 -4
- package/src/app/components/player.tsx +7 -25
- package/src/app/components/present/control-bar.tsx +17 -10
- package/src/app/components/present/help-overlay.tsx +18 -17
- package/src/app/components/present/overview-grid.tsx +6 -9
- package/src/app/components/present/use-presenter-channel.ts +3 -10
- package/src/app/components/sidebar/folder-item.tsx +16 -9
- package/src/app/components/sidebar/icon-picker.tsx +4 -5
- package/src/app/components/sidebar/sidebar.tsx +87 -25
- package/src/app/components/slide-canvas.tsx +1 -10
- package/src/app/components/style-panel/design-provider.tsx +2 -6
- package/src/app/components/style-panel/style-panel.tsx +26 -18
- package/src/app/components/theme-toggle.tsx +7 -5
- package/src/app/components/thumbnail-rail.tsx +4 -2
- package/src/app/favicon.ico +0 -0
- package/src/app/lib/export-html.ts +1 -9
- package/src/app/lib/export-pdf.ts +0 -5
- package/src/app/lib/inspector/use-editor.ts +9 -7
- package/src/app/lib/print-ready.ts +0 -4
- package/src/app/lib/sdk.ts +1 -2
- package/src/app/lib/use-locale.ts +20 -0
- package/src/app/routes/home.tsx +90 -45
- package/src/app/routes/presenter.tsx +45 -25
- package/src/app/routes/slide.tsx +37 -24
- package/src/app/styles.css +28 -0
- package/src/app/virtual.d.ts +4 -0
- package/src/locale/en.ts +303 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +307 -0
- package/src/locale/types.ts +323 -0
- package/src/locale/zh-cn.ts +303 -0
- package/src/locale/zh-tw.ts +303 -0
package/src/app/routes/slide.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '@/components/ui/dropdown-menu';
|
|
26
26
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
27
27
|
import { useFolders } from '@/lib/folders';
|
|
28
|
+
import { useLocale } from '@/lib/use-locale';
|
|
28
29
|
import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
|
|
29
30
|
import { cn } from '@/lib/utils';
|
|
30
31
|
import { ClickNavZones } from '../components/click-nav-zones';
|
|
@@ -49,6 +50,7 @@ export function Slide() {
|
|
|
49
50
|
const [designOpen, setDesignOpen] = useState(false);
|
|
50
51
|
const { renameSlide } = useFolders();
|
|
51
52
|
const slideViewportRef = useRef<HTMLElement>(null);
|
|
53
|
+
const t = useLocale();
|
|
52
54
|
|
|
53
55
|
useEffect(() => {
|
|
54
56
|
let cancelled = false;
|
|
@@ -110,12 +112,12 @@ export function Slide() {
|
|
|
110
112
|
<div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
|
|
111
113
|
{showSlideBrowser && (
|
|
112
114
|
<Link to="/" className="text-[12px] font-medium text-foreground/70 hover:text-foreground">
|
|
113
|
-
←
|
|
115
|
+
← {t.common.home}
|
|
114
116
|
</Link>
|
|
115
117
|
)}
|
|
116
|
-
<span className="mt-6 block eyebrow text-destructive/80">
|
|
118
|
+
<span className="mt-6 block eyebrow text-destructive/80">{t.common.loadFailed}</span>
|
|
117
119
|
<h2 className="mt-2 font-heading text-xl font-semibold tracking-tight text-foreground">
|
|
118
|
-
|
|
120
|
+
{t.common.failedToLoadSlide}
|
|
119
121
|
</h2>
|
|
120
122
|
<pre className="mt-4 overflow-auto rounded-[6px] border border-border bg-card p-4 text-[11.5px] leading-relaxed whitespace-pre-wrap shadow-edge">
|
|
121
123
|
{error}
|
|
@@ -126,9 +128,19 @@ export function Slide() {
|
|
|
126
128
|
|
|
127
129
|
if (!slide) {
|
|
128
130
|
return (
|
|
129
|
-
<div className="
|
|
130
|
-
<
|
|
131
|
-
|
|
131
|
+
<div className="grid min-h-dvh place-items-center px-8 text-muted-foreground">
|
|
132
|
+
<div className="flex flex-col items-center gap-4">
|
|
133
|
+
<div className="relative h-px w-56 overflow-hidden bg-hairline">
|
|
134
|
+
<span
|
|
135
|
+
aria-hidden
|
|
136
|
+
className="line-loader-bar absolute inset-y-[-0.5px] left-0 w-1/4 bg-foreground"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="flex flex-wrap items-baseline justify-center gap-x-2 text-[11.5px]">
|
|
140
|
+
<span className="eyebrow">{t.slide.loadingEyebrow}</span>
|
|
141
|
+
<span className="font-mono">{slideId}</span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
132
144
|
</div>
|
|
133
145
|
);
|
|
134
146
|
}
|
|
@@ -138,22 +150,22 @@ export function Slide() {
|
|
|
138
150
|
<div className="mx-auto max-w-3xl px-8 py-16 text-muted-foreground">
|
|
139
151
|
{showSlideBrowser && (
|
|
140
152
|
<Link to="/" className="text-[12px] font-medium text-foreground/70 hover:text-foreground">
|
|
141
|
-
←
|
|
153
|
+
← {t.common.home}
|
|
142
154
|
</Link>
|
|
143
155
|
)}
|
|
144
|
-
<span className="mt-6 block eyebrow">
|
|
156
|
+
<span className="mt-6 block eyebrow">{t.slide.emptyEyebrow}</span>
|
|
145
157
|
<h2 className="mt-2 font-heading text-xl font-semibold tracking-tight text-foreground">
|
|
146
|
-
|
|
158
|
+
{t.slide.nothingToShow}
|
|
147
159
|
</h2>
|
|
148
160
|
<p className="mt-3 text-[13px] leading-relaxed">
|
|
149
161
|
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px]">
|
|
150
162
|
slides/{slideId}/index.tsx
|
|
151
|
-
</code>
|
|
152
|
-
|
|
163
|
+
</code>
|
|
164
|
+
{t.slide.emptyHintMust}
|
|
153
165
|
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px]">
|
|
154
166
|
export default
|
|
155
|
-
</code>
|
|
156
|
-
|
|
167
|
+
</code>
|
|
168
|
+
{t.slide.emptyHintSuffix}
|
|
157
169
|
</p>
|
|
158
170
|
</div>
|
|
159
171
|
);
|
|
@@ -197,8 +209,8 @@ export function Slide() {
|
|
|
197
209
|
<header className="relative flex h-12 shrink-0 items-center justify-between border-b border-hairline bg-sidebar/85 px-2 backdrop-blur-md md:px-3">
|
|
198
210
|
<div className="flex items-center gap-1.5 md:gap-2">
|
|
199
211
|
{showSlideBrowser && (
|
|
200
|
-
<Button asChild variant="ghost" size="icon-sm" title=
|
|
201
|
-
<Link to="/" aria-label=
|
|
212
|
+
<Button asChild variant="ghost" size="icon-sm" title={t.slide.home}>
|
|
213
|
+
<Link to="/" aria-label={t.slide.backToHome}>
|
|
202
214
|
<ChevronLeft className="size-4" />
|
|
203
215
|
</Link>
|
|
204
216
|
</Button>
|
|
@@ -220,8 +232,8 @@ export function Slide() {
|
|
|
220
232
|
}}
|
|
221
233
|
>
|
|
222
234
|
<TabsList>
|
|
223
|
-
<TabsTrigger value="slides">
|
|
224
|
-
<TabsTrigger value="assets">
|
|
235
|
+
<TabsTrigger value="slides">{t.slide.slidesTab}</TabsTrigger>
|
|
236
|
+
<TabsTrigger value="assets">{t.slide.assetsTab}</TabsTrigger>
|
|
225
237
|
</TabsList>
|
|
226
238
|
</Tabs>
|
|
227
239
|
)}
|
|
@@ -240,8 +252,8 @@ export function Slide() {
|
|
|
240
252
|
<DropdownMenuTrigger
|
|
241
253
|
type="button"
|
|
242
254
|
disabled={exporting}
|
|
243
|
-
aria-label=
|
|
244
|
-
title=
|
|
255
|
+
aria-label={t.slide.download}
|
|
256
|
+
title={t.slide.download}
|
|
245
257
|
className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }))}
|
|
246
258
|
>
|
|
247
259
|
{exporting ? (
|
|
@@ -266,7 +278,7 @@ export function Slide() {
|
|
|
266
278
|
}}
|
|
267
279
|
>
|
|
268
280
|
<FileCode2 />
|
|
269
|
-
|
|
281
|
+
{t.slide.exportAsHtml}
|
|
270
282
|
</DropdownMenuItem>
|
|
271
283
|
<DropdownMenuItem
|
|
272
284
|
disabled={exporting}
|
|
@@ -296,7 +308,7 @@ export function Slide() {
|
|
|
296
308
|
});
|
|
297
309
|
} catch (err) {
|
|
298
310
|
console.error('[open-slide] pdf export failed', err);
|
|
299
|
-
toast.error(
|
|
311
|
+
toast.error(t.slide.pdfExportFailed, { id: toastId, duration: 4000 });
|
|
300
312
|
} finally {
|
|
301
313
|
setExporting(false);
|
|
302
314
|
toast.dismiss(toastId);
|
|
@@ -304,7 +316,7 @@ export function Slide() {
|
|
|
304
316
|
}}
|
|
305
317
|
>
|
|
306
318
|
<FileText />
|
|
307
|
-
|
|
319
|
+
{t.slide.exportAsPdf}
|
|
308
320
|
</DropdownMenuItem>
|
|
309
321
|
</DropdownMenuContent>
|
|
310
322
|
</DropdownMenu>
|
|
@@ -322,7 +334,7 @@ export function Slide() {
|
|
|
322
334
|
className="px-2.5 md:px-3"
|
|
323
335
|
>
|
|
324
336
|
<Play className="size-3.5 fill-current" />
|
|
325
|
-
<span className="hidden md:inline">
|
|
337
|
+
<span className="hidden md:inline">{t.slide.present}</span>
|
|
326
338
|
<kbd className="ml-1 hidden rounded-[3px] bg-brand-foreground/15 px-1 font-mono text-[9.5px] tracking-[0.04em] md:inline">
|
|
327
339
|
F
|
|
328
340
|
</kbd>
|
|
@@ -434,6 +446,7 @@ function InlineTitleEditor({
|
|
|
434
446
|
const [value, setValue] = useState(title);
|
|
435
447
|
const [saving, setSaving] = useState(false);
|
|
436
448
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
449
|
+
const t = useLocale();
|
|
437
450
|
|
|
438
451
|
useEffect(() => {
|
|
439
452
|
if (!editing) setValue(title);
|
|
@@ -505,7 +518,7 @@ function InlineTitleEditor({
|
|
|
505
518
|
<button
|
|
506
519
|
type="button"
|
|
507
520
|
onClick={() => setEditing(true)}
|
|
508
|
-
aria-label=
|
|
521
|
+
aria-label={t.slide.renameSlide}
|
|
509
522
|
className={cn(
|
|
510
523
|
'flex size-5 shrink-0 items-center justify-center rounded-[4px] text-muted-foreground transition-opacity hover:bg-muted hover:text-foreground',
|
|
511
524
|
'opacity-0 group-hover/title:opacity-100 focus-visible:opacity-100',
|
package/src/app/styles.css
CHANGED
|
@@ -348,6 +348,34 @@
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
/*
|
|
352
|
+
* Indeterminate "line loader" — a hairline track with a short bar that
|
|
353
|
+
* glides back and forth. Used for full-screen loading states in place of
|
|
354
|
+
* a spinner; quieter, more typographic, easier on the eye.
|
|
355
|
+
*/
|
|
356
|
+
@keyframes osd-line-loader {
|
|
357
|
+
0% {
|
|
358
|
+
transform: translateX(0%);
|
|
359
|
+
}
|
|
360
|
+
50% {
|
|
361
|
+
transform: translateX(300%);
|
|
362
|
+
}
|
|
363
|
+
100% {
|
|
364
|
+
transform: translateX(0%);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.line-loader-bar {
|
|
369
|
+
animation: osd-line-loader 1.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@media (prefers-reduced-motion: reduce) {
|
|
373
|
+
.line-loader-bar {
|
|
374
|
+
animation: none;
|
|
375
|
+
transform: translateX(150%);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
351
379
|
[data-osd-freeze-motion],
|
|
352
380
|
[data-osd-freeze-motion] *,
|
|
353
381
|
[data-osd-freeze-motion] *::before,
|
package/src/app/virtual.d.ts
CHANGED
|
@@ -5,9 +5,12 @@ declare module 'virtual:open-slide/slides' {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
declare module 'virtual:open-slide/config' {
|
|
8
|
+
import type { Locale } from '../locale/types';
|
|
9
|
+
|
|
8
10
|
const config: {
|
|
9
11
|
slidesDir?: string;
|
|
10
12
|
port?: number;
|
|
13
|
+
locale?: Locale;
|
|
11
14
|
build: {
|
|
12
15
|
showSlideBrowser: boolean;
|
|
13
16
|
showSlideUi: boolean;
|
|
@@ -19,6 +22,7 @@ declare module 'virtual:open-slide/config' {
|
|
|
19
22
|
|
|
20
23
|
declare module 'virtual:open-slide/folders' {
|
|
21
24
|
import type { FoldersManifest } from './lib/sdk';
|
|
25
|
+
|
|
22
26
|
const manifest: FoldersManifest;
|
|
23
27
|
export default manifest;
|
|
24
28
|
}
|
package/src/locale/en.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { Locale } from './types';
|
|
2
|
+
|
|
3
|
+
export const en: Locale = {
|
|
4
|
+
id: 'en',
|
|
5
|
+
|
|
6
|
+
common: {
|
|
7
|
+
cancel: 'Cancel',
|
|
8
|
+
save: 'Save',
|
|
9
|
+
saving: 'Saving',
|
|
10
|
+
saved: 'Saved',
|
|
11
|
+
discard: 'Discard',
|
|
12
|
+
delete: 'Delete',
|
|
13
|
+
rename: 'Rename',
|
|
14
|
+
move: 'Move',
|
|
15
|
+
close: 'Close',
|
|
16
|
+
loading: 'Loading',
|
|
17
|
+
loadFailed: 'Load failed',
|
|
18
|
+
failedToLoadSlide: 'Failed to load slide',
|
|
19
|
+
home: 'Home',
|
|
20
|
+
backToHome: 'Back to home',
|
|
21
|
+
preview: 'Preview',
|
|
22
|
+
add: 'Add',
|
|
23
|
+
done: 'Done',
|
|
24
|
+
tryAgain: 'Try again',
|
|
25
|
+
undo: 'Undo',
|
|
26
|
+
redo: 'Redo',
|
|
27
|
+
light: 'Light',
|
|
28
|
+
dark: 'Dark',
|
|
29
|
+
system: 'System',
|
|
30
|
+
selected: 'Selected',
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
notFound: {
|
|
34
|
+
eyebrow: '404 · not found',
|
|
35
|
+
title: 'Page not found',
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
home: {
|
|
39
|
+
appTitle: 'open-slide',
|
|
40
|
+
draft: 'Draft',
|
|
41
|
+
folders: 'Folders',
|
|
42
|
+
newFolder: 'New folder',
|
|
43
|
+
folderName: 'Folder name',
|
|
44
|
+
changeIcon: 'Change icon',
|
|
45
|
+
iconEmojiTab: 'Emoji',
|
|
46
|
+
iconColorTab: 'Color',
|
|
47
|
+
folderActions: 'Folder actions',
|
|
48
|
+
searchPlaceholder: 'Search slides',
|
|
49
|
+
clearSearch: 'Clear search',
|
|
50
|
+
noMatches: 'No matches',
|
|
51
|
+
nothingMatchesPrefix: 'Nothing matches ',
|
|
52
|
+
nothingMatchesSuffix: ' in this folder.',
|
|
53
|
+
noSlidesYet: 'No slides yet',
|
|
54
|
+
createSlideHintPrefix: 'Create ',
|
|
55
|
+
createSlideHintMid: ' that ',
|
|
56
|
+
createSlideHintSuffix: '.',
|
|
57
|
+
folderEmptyTitle: '{name} is empty',
|
|
58
|
+
folderEmptyHint: 'Drag a slide from Draft into this folder in the sidebar.',
|
|
59
|
+
slideActions: 'Slide actions',
|
|
60
|
+
moveToFolder: 'Move to folder…',
|
|
61
|
+
renameDialogEyebrow: 'Rename',
|
|
62
|
+
renameDialogTitle: 'Rename slide',
|
|
63
|
+
renameDialogDescription: 'Give this slide a new display name.',
|
|
64
|
+
slideNamePlaceholder: 'Slide name',
|
|
65
|
+
moveDialogEyebrow: 'Move',
|
|
66
|
+
moveDialogTitle: 'Move slide',
|
|
67
|
+
moveDialogDescriptionPrefix: 'Choose a folder for ',
|
|
68
|
+
moveDialogDescriptionSuffix: '.',
|
|
69
|
+
deleteDialogEyebrow: 'Destructive',
|
|
70
|
+
deleteDialogTitle: 'Delete slide?',
|
|
71
|
+
deleteDialogDescriptionPrefix: 'This permanently removes ',
|
|
72
|
+
deleteDialogDescriptionMid: ' and its files from disk. ',
|
|
73
|
+
deleteDialogDescriptionSuffix: 'This action cannot be undone.',
|
|
74
|
+
toastFolderCreated: 'Created folder “{name}”',
|
|
75
|
+
toastFolderCreateFailed: 'Failed to create folder',
|
|
76
|
+
toastSlideMoved: 'Moved “{slide}” to {folder}',
|
|
77
|
+
toastSlideMoveFailed: 'Failed to move slide',
|
|
78
|
+
toastFolderDeleted: 'Deleted folder “{name}”',
|
|
79
|
+
toastFolderDeleteFailed: 'Failed to delete folder',
|
|
80
|
+
pickIcon: 'Pick icon',
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
slide: {
|
|
84
|
+
home: 'Home',
|
|
85
|
+
backToHome: 'Back to home',
|
|
86
|
+
download: 'Download',
|
|
87
|
+
exportAsHtml: 'Export as HTML',
|
|
88
|
+
exportAsPdf: 'Export as PDF',
|
|
89
|
+
pdfExportFailed: 'PDF export failed',
|
|
90
|
+
present: 'Present',
|
|
91
|
+
slidesTab: 'Slides',
|
|
92
|
+
assetsTab: 'Assets',
|
|
93
|
+
renameSlide: 'Rename slide',
|
|
94
|
+
loadingEyebrow: 'Loading',
|
|
95
|
+
emptyEyebrow: 'Empty',
|
|
96
|
+
nothingToShow: 'Nothing to show.',
|
|
97
|
+
emptyHintPrefix: '',
|
|
98
|
+
emptyHintMust: ' must ',
|
|
99
|
+
emptyHintSuffix: ' a non-empty array of components.',
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
presenter: {
|
|
103
|
+
eyebrow: 'Presenter',
|
|
104
|
+
notLinked: 'Not linked',
|
|
105
|
+
nowShowing: 'Now showing',
|
|
106
|
+
upNext: 'Up next',
|
|
107
|
+
lastSlide: 'Last slide',
|
|
108
|
+
endOfDeck: 'End of deck',
|
|
109
|
+
speakerNotes: 'Speaker notes',
|
|
110
|
+
noNotesPrefix: 'No speaker notes for this slide. Add ',
|
|
111
|
+
noNotesSuffix: ' to your slide module to see notes here.',
|
|
112
|
+
blackScreen: 'Black screen',
|
|
113
|
+
whiteScreen: 'White screen',
|
|
114
|
+
prev: 'Prev',
|
|
115
|
+
next: 'Next',
|
|
116
|
+
black: 'Black',
|
|
117
|
+
white: 'White',
|
|
118
|
+
reset: 'Reset',
|
|
119
|
+
resetTimer: 'Reset timer',
|
|
120
|
+
currentTime: 'Current time',
|
|
121
|
+
elapsed: 'Elapsed',
|
|
122
|
+
jump: 'Jump',
|
|
123
|
+
loadingSlide: 'Loading {slideId}…',
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
present: {
|
|
127
|
+
prevSlideAria: 'Previous slide (←)',
|
|
128
|
+
nextSlideAria: 'Next slide (→)',
|
|
129
|
+
overviewAria: 'Slide overview (O)',
|
|
130
|
+
blackoutAria: 'Black screen (B)',
|
|
131
|
+
whiteoutAria: 'White screen (W)',
|
|
132
|
+
laserAria: 'Laser pointer (L)',
|
|
133
|
+
presenterAria: 'Presenter view (P)',
|
|
134
|
+
helpAria: 'Keyboard shortcuts (?)',
|
|
135
|
+
exitAria: 'Exit (Esc)',
|
|
136
|
+
elapsedTime: 'Elapsed time',
|
|
137
|
+
helpEyebrow: 'Present mode',
|
|
138
|
+
helpTitle: 'Keyboard shortcuts',
|
|
139
|
+
shortcutNext: 'Next slide',
|
|
140
|
+
shortcutPrev: 'Previous slide',
|
|
141
|
+
shortcutFirstLast: 'First / last slide',
|
|
142
|
+
shortcutJump: 'Jump to slide',
|
|
143
|
+
shortcutOverview: 'Slide overview',
|
|
144
|
+
shortcutBlack: 'Black screen',
|
|
145
|
+
shortcutWhite: 'White screen',
|
|
146
|
+
shortcutLaser: 'Laser pointer',
|
|
147
|
+
shortcutPresenter: 'Open Presenter View',
|
|
148
|
+
shortcutToggleHelp: 'Toggle this help',
|
|
149
|
+
shortcutCloseExit: 'Close overlay / exit',
|
|
150
|
+
overviewDialogAria: 'Slide overview',
|
|
151
|
+
overviewEyebrow: 'Overview',
|
|
152
|
+
overviewGoToAria: 'Go to slide {n}',
|
|
153
|
+
nowBadge: 'Now',
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
inspector: {
|
|
157
|
+
inspect: 'Inspect',
|
|
158
|
+
deselect: 'Deselect',
|
|
159
|
+
contentSection: 'Content',
|
|
160
|
+
typographySection: 'Typography',
|
|
161
|
+
colorSection: 'Color',
|
|
162
|
+
textColor: 'Text',
|
|
163
|
+
backgroundColor: 'Background',
|
|
164
|
+
imageSection: 'Image',
|
|
165
|
+
imagePlaceholderSection: 'Image placeholder',
|
|
166
|
+
elementTextPlaceholder: 'Element text',
|
|
167
|
+
sizeLabel: 'Size',
|
|
168
|
+
weightLabel: 'Weight',
|
|
169
|
+
weightLight: 'Light · 300',
|
|
170
|
+
weightRegular: 'Regular · 400',
|
|
171
|
+
weightMedium: 'Medium · 500',
|
|
172
|
+
weightSemibold: 'Semibold · 600',
|
|
173
|
+
weightBold: 'Bold · 700',
|
|
174
|
+
weightExtrabold: 'Extrabold · 800',
|
|
175
|
+
styleLabel: 'Style',
|
|
176
|
+
boldAria: 'Bold',
|
|
177
|
+
italicAria: 'Italic',
|
|
178
|
+
lineHeightLabel: 'Line height',
|
|
179
|
+
trackingLabel: 'Tracking',
|
|
180
|
+
alignLabel: 'Align',
|
|
181
|
+
clearAria: 'Clear',
|
|
182
|
+
replace: 'Replace…',
|
|
183
|
+
replaceImageDialogTitle: 'Replace image',
|
|
184
|
+
replaceImageDescription: 'Pick an asset from {path}.',
|
|
185
|
+
pickerLoading: 'Loading…',
|
|
186
|
+
pickerEmpty: "No images in this slide's assets folder yet. Add some from the Assets tab.",
|
|
187
|
+
placeholderHintLabel: 'Hint:',
|
|
188
|
+
noteForAgent: 'Note for the agent',
|
|
189
|
+
noteAgentPlaceholder: 'Describe a change for the agent…',
|
|
190
|
+
noteShortcutHint: '⌘↵ to send',
|
|
191
|
+
addNote: 'Add note',
|
|
192
|
+
unsavedChanges: {
|
|
193
|
+
one: '{count} unsaved change',
|
|
194
|
+
other: '{count} unsaved changes',
|
|
195
|
+
},
|
|
196
|
+
commentsCount: {
|
|
197
|
+
one: '{count} comment',
|
|
198
|
+
other: '{count} comments',
|
|
199
|
+
},
|
|
200
|
+
commentLineLabel: 'line {n}',
|
|
201
|
+
commentsEmpty: 'No comments yet. Toggle Inspect and click a slide element.',
|
|
202
|
+
commentsApplyHintPrefix: 'Run ',
|
|
203
|
+
commentsApplyHintSuffix: ' in your agent to apply these.',
|
|
204
|
+
commentDeleteAria: 'Delete',
|
|
205
|
+
saveFailed: "Couldn't save:",
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
stylePanel: {
|
|
209
|
+
designTokens: 'Design tokens',
|
|
210
|
+
draftBadge: 'draft',
|
|
211
|
+
unsavedTitle: 'Unsaved',
|
|
212
|
+
closePanelAria: 'Close design panel',
|
|
213
|
+
colorsSection: 'Colors',
|
|
214
|
+
typographySection: 'Typography',
|
|
215
|
+
shapeSection: 'Shape',
|
|
216
|
+
backgroundLabel: 'Background',
|
|
217
|
+
textLabel: 'Text',
|
|
218
|
+
accentLabel: 'Accent',
|
|
219
|
+
displayFontLabel: 'Display',
|
|
220
|
+
bodyFontLabel: 'Body',
|
|
221
|
+
heroLabel: 'Hero',
|
|
222
|
+
bodyLabel: 'Body',
|
|
223
|
+
radiusLabel: 'Radius',
|
|
224
|
+
designToggle: 'Design',
|
|
225
|
+
designToggleTitle: 'Design tokens',
|
|
226
|
+
fontPresetCustom: 'Custom…',
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
asset: {
|
|
230
|
+
devOnlyMessage: 'Asset management is only available in dev mode.',
|
|
231
|
+
sectionAria: 'Slide assets',
|
|
232
|
+
eyebrow: 'Assets',
|
|
233
|
+
fileCount: { one: '{count} file', other: '{count} files' },
|
|
234
|
+
searchLogos: 'Search logos',
|
|
235
|
+
upload: 'Upload',
|
|
236
|
+
dropToUpload: 'Drop to upload',
|
|
237
|
+
loading: 'Loading…',
|
|
238
|
+
noAssetsYet: 'No assets yet',
|
|
239
|
+
noAssetsHintPrefix: 'Drop files anywhere here, or use ',
|
|
240
|
+
noAssetsHintSuffix: '.',
|
|
241
|
+
nameAlreadyExists: 'A file with that name already exists.',
|
|
242
|
+
previewAria: 'Preview {name}',
|
|
243
|
+
actionsAria: 'Actions for {name}',
|
|
244
|
+
previewMenuItem: 'Preview',
|
|
245
|
+
renameMenuItem: 'Rename',
|
|
246
|
+
deleteMenuItem: 'Delete',
|
|
247
|
+
conflictTitle: 'File already exists',
|
|
248
|
+
conflictDescription: "{name} is already in this slide's assets folder.",
|
|
249
|
+
conflictReplace: 'Replace',
|
|
250
|
+
conflictRenameCopy: 'Rename copy',
|
|
251
|
+
deleteAssetTitle: 'Delete asset',
|
|
252
|
+
deleteAssetDescription: 'Delete {name}? Imports referencing this file in the slide will break.',
|
|
253
|
+
noPreview: 'No preview available',
|
|
254
|
+
importHintComment: 'import asset from ',
|
|
255
|
+
importHintSemi: ';',
|
|
256
|
+
logoSearchTitle: 'Search logos',
|
|
257
|
+
logoSearchPoweredByPrefix: 'Powered by ',
|
|
258
|
+
logoSearchPlaceholder: 'Search by brand…',
|
|
259
|
+
logoSearchErrorTitle: "Couldn't reach svgl",
|
|
260
|
+
logoSearchErrorBody: 'Check your connection and try again.',
|
|
261
|
+
logoSearchNoResults: 'No logos for "{query}"',
|
|
262
|
+
logoSearchEmpty: 'No logos available',
|
|
263
|
+
logoSearchEmptyHintPrefix: 'Try a different brand name, or browse the full catalog at ',
|
|
264
|
+
logoSearchEmptyHintSuffix: '.',
|
|
265
|
+
logoVariantLight: 'Light',
|
|
266
|
+
logoVariantDark: 'Dark',
|
|
267
|
+
toastUploadFailed: 'Upload failed ({status})',
|
|
268
|
+
toastReplaced: 'Replaced {name}',
|
|
269
|
+
toastUploadedAs: 'Uploaded as {name}',
|
|
270
|
+
toastUploaded: 'Uploaded {name}',
|
|
271
|
+
toastRenameFailed: 'Rename failed ({status})',
|
|
272
|
+
toastRenamed: 'Renamed to {name}',
|
|
273
|
+
toastDeleteFailed: 'Delete failed ({status})',
|
|
274
|
+
toastDeleted: 'Deleted {name}',
|
|
275
|
+
toastDownloadFailed: 'Failed to download logo',
|
|
276
|
+
toastSearchFailed: 'Search failed',
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
thumbnailRail: {
|
|
280
|
+
pages: 'Pages',
|
|
281
|
+
goToPageAria: 'Go to page {n}',
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
pdfToast: {
|
|
285
|
+
title: 'Exporting PDF',
|
|
286
|
+
processing: 'Processing page {current} of {total}',
|
|
287
|
+
printing: 'Opening print dialog…',
|
|
288
|
+
done: 'Done',
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
themeToggle: {
|
|
292
|
+
toggleAria: 'Toggle theme',
|
|
293
|
+
title: 'Theme',
|
|
294
|
+
light: 'Light',
|
|
295
|
+
dark: 'Dark',
|
|
296
|
+
system: 'System',
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
clickNav: {
|
|
300
|
+
prevAria: 'Previous page',
|
|
301
|
+
nextAria: 'Next page',
|
|
302
|
+
},
|
|
303
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Plural } from './types';
|
|
2
|
+
|
|
3
|
+
export function format(template: string, vars: Record<string, string | number>): string {
|
|
4
|
+
return template.replace(/\{(\w+)\}/g, (m, key) => {
|
|
5
|
+
const v = vars[key];
|
|
6
|
+
return v === undefined ? m : String(v);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function plural(count: number, forms: Plural): string {
|
|
11
|
+
return count === 1 ? forms.one : forms.other;
|
|
12
|
+
}
|