feat(i18n): localize Stories tab (list, content, dialogs, toasts)

Covers the title + "New Story" button, empty states, story row
metadata (item count, updated time), the create/edit/delete dialogs,
and all toast notifications. Also handles StoryContent: "Select a
story" placeholder, search popover, "Export Audio" button, and the
"Generating N audios" pending indicator.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
James Pine
2026-04-20 03:02:21 -07:00
co-authored by Claude Opus 4.7
parent dd4119e0e6
commit 992ad96a29
4 changed files with 215 additions and 64 deletions
+20 -16
View File
@@ -17,6 +17,7 @@ import { Link } from '@tanstack/react-router';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Download, Plus } from 'lucide-react'; import { Download, Plus } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Loader from 'react-loaders'; import Loader from 'react-loaders';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -36,6 +37,7 @@ import { useStoryStore } from '@/stores/storyStore';
import { SortableStoryChatItem } from './StoryChatItem'; import { SortableStoryChatItem } from './StoryChatItem';
export function StoryContent() { export function StoryContent() {
const { t } = useTranslation();
const selectedStoryId = useStoryStore((state) => state.selectedStoryId); const selectedStoryId = useStoryStore((state) => state.selectedStoryId);
const { data: story, isLoading } = useStory(selectedStoryId); const { data: story, isLoading } = useStory(selectedStoryId);
const removeItem = useRemoveStoryItem(); const removeItem = useRemoveStoryItem();
@@ -147,7 +149,7 @@ export function StoryContent() {
{ {
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to remove item', title: t('storyContent.toast.removeFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -179,7 +181,7 @@ export function StoryContent() {
{ {
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to reorder items', title: t('storyContent.toast.reorderFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -199,7 +201,7 @@ export function StoryContent() {
{ {
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to export audio', title: t('storyContent.toast.exportFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -223,7 +225,7 @@ export function StoryContent() {
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to add generation', title: t('storyContent.toast.addFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -236,8 +238,8 @@ export function StoryContent() {
return ( return (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center"> <div className="text-center">
<p className="text-lg font-medium mb-2">Select a story</p> <p className="text-lg font-medium mb-2">{t('storyContent.selectStory.title')}</p>
<p className="text-sm">Choose a story from the list to view its content</p> <p className="text-sm">{t('storyContent.selectStory.hint')}</p>
</div> </div>
</div> </div>
); );
@@ -246,7 +248,7 @@ export function StoryContent() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-muted-foreground">Loading story...</div> <div className="text-muted-foreground">{t('storyContent.loading')}</div>
</div> </div>
); );
} }
@@ -255,8 +257,8 @@ export function StoryContent() {
return ( return (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center"> <div className="text-center">
<p className="text-lg font-medium mb-2">Story not found</p> <p className="text-lg font-medium mb-2">{t('storyContent.notFound.title')}</p>
<p className="text-sm">The selected story could not be loaded</p> <p className="text-sm">{t('storyContent.notFound.hint')}</p>
</div> </div>
</div> </div>
); );
@@ -291,7 +293,7 @@ export function StoryContent() {
</div> </div>
</div> </div>
<span className="text-xs text-muted-foreground whitespace-nowrap"> <span className="text-xs text-muted-foreground whitespace-nowrap">
Generating {pendingCount} {pendingCount === 1 ? 'audio' : 'audios'} {t('storyContent.generatingCount', { count: pendingCount })}
</span> </span>
</Link> </Link>
</motion.div> </motion.div>
@@ -301,13 +303,13 @@ export function StoryContent() {
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Add {t('storyContent.add')}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end"> <PopoverContent className="w-80 p-0" align="end">
<div className="p-2 border-b"> <div className="p-2 border-b">
<Input <Input
placeholder="Search by name or transcript..." placeholder={t('storyContent.searchPlaceholder')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
autoFocus autoFocus
@@ -316,7 +318,9 @@ export function StoryContent() {
<div className="max-h-60 overflow-y-auto"> <div className="max-h-60 overflow-y-auto">
{availableGenerations.length === 0 ? ( {availableGenerations.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground"> <div className="p-4 text-center text-sm text-muted-foreground">
{searchQuery ? 'No matching generations found' : 'No available generations'} {searchQuery
? t('storyContent.searchNoMatches')
: t('storyContent.searchNoAvailable')}
</div> </div>
) : ( ) : (
availableGenerations.map((gen) => ( availableGenerations.map((gen) => (
@@ -344,7 +348,7 @@ export function StoryContent() {
disabled={exportAudio.isPending} disabled={exportAudio.isPending}
> >
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Export Audio {t('storyContent.exportAudio')}
</Button> </Button>
)} )}
</div> </div>
@@ -358,8 +362,8 @@ export function StoryContent() {
> >
{sortedItems.length === 0 ? ( {sortedItems.length === 0 ? (
<div className="text-center py-12 px-5 border-2 border-dashed border-muted rounded-md text-muted-foreground"> <div className="text-center py-12 px-5 border-2 border-dashed border-muted rounded-md text-muted-foreground">
<p className="text-sm">No items in this story</p> <p className="text-sm">{t('storyContent.empty.title')}</p>
<p className="text-xs mt-2">Generate speech using the box below to add items</p> <p className="text-xs mt-2">{t('storyContent.empty.hint')}</p>
</div> </div>
) : ( ) : (
<DndContext <DndContext
+47 -48
View File
@@ -1,5 +1,6 @@
import { BookOpen, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react'; import { BookOpen, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -41,6 +42,7 @@ import { formatDate } from '@/lib/utils/format';
import { useStoryStore } from '@/stores/storyStore'; import { useStoryStore } from '@/stores/storyStore';
export function StoryList() { export function StoryList() {
const { t } = useTranslation();
const { data: stories, isLoading } = useStories(); const { data: stories, isLoading } = useStories();
const selectedStoryId = useStoryStore((state) => state.selectedStoryId); const selectedStoryId = useStoryStore((state) => state.selectedStoryId);
const setSelectedStoryId = useStoryStore((state) => state.setSelectedStoryId); const setSelectedStoryId = useStoryStore((state) => state.setSelectedStoryId);
@@ -72,8 +74,8 @@ export function StoryList() {
const handleCreateStory = () => { const handleCreateStory = () => {
if (!newStoryName.trim()) { if (!newStoryName.trim()) {
toast({ toast({
title: 'Name required', title: t('stories.toast.nameRequired'),
description: 'Please enter a story name', description: t('stories.toast.nameRequiredDescription'),
variant: 'destructive', variant: 'destructive',
}); });
return; return;
@@ -91,13 +93,13 @@ export function StoryList() {
setNewStoryName(''); setNewStoryName('');
setNewStoryDescription(''); setNewStoryDescription('');
toast({ toast({
title: 'Story created', title: t('stories.toast.created'),
description: `"${story.name}" has been created`, description: t('stories.toast.createdDescription', { name: story.name }),
}); });
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to create story', title: t('stories.toast.createFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -116,8 +118,8 @@ export function StoryList() {
const handleUpdateStory = () => { const handleUpdateStory = () => {
if (!editingStory || !newStoryName.trim()) { if (!editingStory || !newStoryName.trim()) {
toast({ toast({
title: 'Name required', title: t('stories.toast.nameRequired'),
description: 'Please enter a story name', description: t('stories.toast.nameRequiredDescription'),
variant: 'destructive', variant: 'destructive',
}); });
return; return;
@@ -140,7 +142,7 @@ export function StoryList() {
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to update story', title: t('stories.toast.updateFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -168,7 +170,7 @@ export function StoryList() {
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: 'Failed to delete story', title: t('stories.toast.deleteFailed'),
description: error.message, description: error.message,
variant: 'destructive', variant: 'destructive',
}); });
@@ -179,7 +181,7 @@ export function StoryList() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-muted-foreground">Loading stories...</div> <div className="text-muted-foreground">{t('stories.loading')}</div>
</div> </div>
); );
} }
@@ -195,10 +197,10 @@ export function StoryList() {
{/* Fixed Header */} {/* Fixed Header */}
<div className="absolute top-0 left-0 right-0 z-20"> <div className="absolute top-0 left-0 right-0 z-20">
<div className="flex items-center justify-between mb-4 px-1"> <div className="flex items-center justify-between mb-4 px-1">
<h2 className="text-2xl font-bold">Stories</h2> <h2 className="text-2xl font-bold">{t('stories.title')}</h2>
<Button onClick={() => setCreateDialogOpen(true)} size="sm"> <Button onClick={() => setCreateDialogOpen(true)} size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
New Story {t('stories.newStory')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -211,8 +213,8 @@ export function StoryList() {
{storyList.length === 0 ? ( {storyList.length === 0 ? (
<div className="text-center py-12 px-5 border-2 border-dashed border-muted rounded-2xl text-muted-foreground"> <div className="text-center py-12 px-5 border-2 border-dashed border-muted rounded-2xl text-muted-foreground">
<BookOpen className="h-12 w-12 mx-auto mb-4 opacity-50" /> <BookOpen className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="text-sm">No stories yet</p> <p className="text-sm">{t('stories.empty.title')}</p>
<p className="text-xs mt-2">Create your first story to get started</p> <p className="text-xs mt-2">{t('stories.empty.hint')}</p>
</div> </div>
) : ( ) : (
<div className="space-y-0.5"> <div className="space-y-0.5">
@@ -225,7 +227,11 @@ export function StoryList() {
'px-5 py-3 rounded-lg transition-colors group flex items-center cursor-pointer', 'px-5 py-3 rounded-lg transition-colors group flex items-center cursor-pointer',
selectedStoryId === story.id ? 'bg-muted' : 'hover:bg-muted/50', selectedStoryId === story.id ? 'bg-muted' : 'hover:bg-muted/50',
)} )}
aria-label={`Story ${story.name}, ${story.item_count} ${story.item_count === 1 ? 'item' : 'items'}, ${formatDate(story.updated_at)}`} aria-label={t('stories.row.ariaLabel', {
name: story.name,
count: story.item_count,
updated: formatDate(story.updated_at),
})}
aria-pressed={selectedStoryId === story.id} aria-pressed={selectedStoryId === story.id}
onClick={() => setSelectedStoryId(story.id)} onClick={() => setSelectedStoryId(story.id)}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -240,9 +246,7 @@ export function StoryList() {
<div className="flex-1 min-w-0 text-left overflow-hidden"> <div className="flex-1 min-w-0 text-left overflow-hidden">
<h3 className="text-sm font-medium truncate">{story.name}</h3> <h3 className="text-sm font-medium truncate">{story.name}</h3>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground"> <div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
<span> <span>{t('stories.row.itemCount', { count: story.item_count })}</span>
{story.item_count} {story.item_count === 1 ? 'item' : 'items'}
</span>
<span>·</span> <span>·</span>
<span>{formatDate(story.updated_at)}</span> <span>{formatDate(story.updated_at)}</span>
</div> </div>
@@ -254,7 +258,7 @@ export function StoryList() {
size="icon" size="icon"
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity" className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
aria-label={`Actions for ${story.name}`} aria-label={t('stories.row.actionsLabel', { name: story.name })}
> >
<MoreHorizontal className="h-3.5 w-3.5" /> <MoreHorizontal className="h-3.5 w-3.5" />
</Button> </Button>
@@ -262,14 +266,14 @@ export function StoryList() {
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEditClick(story)}> <DropdownMenuItem onClick={() => handleEditClick(story)}>
<Pencil className="mr-2 h-4 w-4" /> <Pencil className="mr-2 h-4 w-4" />
Edit {t('common.edit')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDeleteClick(story.id)} onClick={() => handleDeleteClick(story.id)}
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Delete {t('common.delete')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -284,17 +288,15 @@ export function StoryList() {
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}> <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create New Story</DialogTitle> <DialogTitle>{t('stories.createDialog.title')}</DialogTitle>
<DialogDescription> <DialogDescription>{t('stories.createDialog.description')}</DialogDescription>
Create a new story to organize your voice generations into conversations.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="story-name">Name</Label> <Label htmlFor="story-name">{t('stories.fields.name')}</Label>
<Input <Input
id="story-name" id="story-name"
placeholder="My Story" placeholder={t('stories.fields.namePlaceholder')}
value={newStoryName} value={newStoryName}
onChange={(e) => setNewStoryName(e.target.value)} onChange={(e) => setNewStoryName(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -305,10 +307,10 @@ export function StoryList() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="story-description">Description (optional)</Label> <Label htmlFor="story-description">{t('stories.fields.descriptionLabel')}</Label>
<Textarea <Textarea
id="story-description" id="story-description"
placeholder="A conversation between..." placeholder={t('stories.fields.descriptionPlaceholder')}
value={newStoryDescription} value={newStoryDescription}
onChange={(e) => setNewStoryDescription(e.target.value)} onChange={(e) => setNewStoryDescription(e.target.value)}
rows={3} rows={3}
@@ -317,28 +319,29 @@ export function StoryList() {
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}> <Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
Cancel {t('common.cancel')}
</Button> </Button>
<Button onClick={handleCreateStory} disabled={createStory.isPending}> <Button onClick={handleCreateStory} disabled={createStory.isPending}>
{createStory.isPending ? 'Creating...' : 'Create'} {createStory.isPending
? t('stories.createDialog.creating')
: t('stories.createDialog.action')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Edit Story Dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}> <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Edit Story</DialogTitle> <DialogTitle>{t('stories.editDialog.title')}</DialogTitle>
<DialogDescription>Update the story name and description.</DialogDescription> <DialogDescription>{t('stories.editDialog.description')}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-story-name">Name</Label> <Label htmlFor="edit-story-name">{t('stories.fields.name')}</Label>
<Input <Input
id="edit-story-name" id="edit-story-name"
placeholder="My Story" placeholder={t('stories.fields.namePlaceholder')}
value={newStoryName} value={newStoryName}
onChange={(e) => setNewStoryName(e.target.value)} onChange={(e) => setNewStoryName(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -349,10 +352,10 @@ export function StoryList() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-story-description">Description (optional)</Label> <Label htmlFor="edit-story-description">{t('stories.fields.descriptionLabel')}</Label>
<Textarea <Textarea
id="edit-story-description" id="edit-story-description"
placeholder="A conversation between..." placeholder={t('stories.fields.descriptionPlaceholder')}
value={newStoryDescription} value={newStoryDescription}
onChange={(e) => setNewStoryDescription(e.target.value)} onChange={(e) => setNewStoryDescription(e.target.value)}
rows={3} rows={3}
@@ -361,34 +364,30 @@ export function StoryList() {
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setEditDialogOpen(false)}> <Button variant="outline" onClick={() => setEditDialogOpen(false)}>
Cancel {t('common.cancel')}
</Button> </Button>
<Button onClick={handleUpdateStory} disabled={updateStory.isPending}> <Button onClick={handleUpdateStory} disabled={updateStory.isPending}>
{updateStory.isPending ? 'Saving...' : 'Save'} {updateStory.isPending ? t('stories.editDialog.saving') : t('common.save')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete Story Confirmation Dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle> <AlertDialogTitle>{t('stories.deleteDialog.title')}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>{t('stories.deleteDialog.description')}</AlertDialogDescription>
This will permanently delete the story and all its items. This action cannot be
undone.
</AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction asChild> <AlertDialogAction asChild>
<Button <Button
onClick={handleDeleteConfirm} onClick={handleDeleteConfirm}
disabled={deleteStory.isPending} disabled={deleteStory.isPending}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
> >
{deleteStory.isPending ? 'Deleting...' : 'Delete'} {deleteStory.isPending ? t('stories.deleteDialog.deleting') : t('common.delete')}
</Button> </Button>
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
+74
View File
@@ -37,6 +37,80 @@
"deleting": "Deleting…" "deleting": "Deleting…"
} }
}, },
"stories": {
"title": "Stories",
"newStory": "New Story",
"loading": "Loading stories…",
"empty": {
"title": "No stories yet",
"hint": "Create your first story to get started"
},
"row": {
"itemCount_one": "{{count}} item",
"itemCount_other": "{{count}} items",
"ariaLabel": "Story {{name}}, {{count}} items, {{updated}}",
"actionsLabel": "Actions for {{name}}"
},
"createDialog": {
"title": "Create New Story",
"description": "Create a new story to organize your voice generations into conversations.",
"action": "Create",
"creating": "Creating…"
},
"editDialog": {
"title": "Edit Story",
"description": "Update the story name and description.",
"saving": "Saving…"
},
"deleteDialog": {
"title": "Are you sure?",
"description": "This will permanently delete the story and all its items. This action cannot be undone.",
"deleting": "Deleting…"
},
"fields": {
"name": "Name",
"namePlaceholder": "My Story",
"descriptionLabel": "Description (optional)",
"descriptionPlaceholder": "A conversation between…"
},
"toast": {
"nameRequired": "Name required",
"nameRequiredDescription": "Please enter a story name",
"created": "Story created",
"createdDescription": "\"{{name}}\" has been created",
"createFailed": "Failed to create story",
"updateFailed": "Failed to update story",
"deleteFailed": "Failed to delete story"
}
},
"storyContent": {
"selectStory": {
"title": "Select a story",
"hint": "Choose a story from the list to view its content"
},
"loading": "Loading story…",
"notFound": {
"title": "Story not found",
"hint": "The selected story could not be loaded"
},
"generatingCount_one": "Generating {{count}} audio",
"generatingCount_other": "Generating {{count}} audios",
"add": "Add",
"searchPlaceholder": "Search by name or transcript…",
"searchNoMatches": "No matching generations found",
"searchNoAvailable": "No available generations",
"exportAudio": "Export Audio",
"empty": {
"title": "No items in this story",
"hint": "Generate speech using the box below to add items"
},
"toast": {
"removeFailed": "Failed to remove item",
"reorderFailed": "Failed to reorder items",
"exportFailed": "Failed to export audio",
"addFailed": "Failed to add generation"
}
},
"history": { "history": {
"deleteDialog": { "deleteDialog": {
"title": "Delete Generation", "title": "Delete Generation",
@@ -37,6 +37,80 @@
"deleting": "删除中…" "deleting": "删除中…"
} }
}, },
"stories": {
"title": "故事",
"newStory": "新建故事",
"loading": "加载故事中…",
"empty": {
"title": "暂无故事",
"hint": "创建您的第一个故事以开始"
},
"row": {
"itemCount_one": "{{count}} 项",
"itemCount_other": "{{count}} 项",
"ariaLabel": "故事 {{name}},{{count}} 项,{{updated}}",
"actionsLabel": "{{name}} 的操作"
},
"createDialog": {
"title": "新建故事",
"description": "创建新故事以将您的语音生成整理成对话。",
"action": "创建",
"creating": "创建中…"
},
"editDialog": {
"title": "编辑故事",
"description": "更新故事名称和描述。",
"saving": "保存中…"
},
"deleteDialog": {
"title": "确定吗?",
"description": "这将永久删除该故事及其所有项目。此操作不可撤销。",
"deleting": "删除中…"
},
"fields": {
"name": "名称",
"namePlaceholder": "我的故事",
"descriptionLabel": "描述(可选)",
"descriptionPlaceholder": "一段对话……"
},
"toast": {
"nameRequired": "请输入名称",
"nameRequiredDescription": "请输入故事名称",
"created": "故事已创建",
"createdDescription": "\"{{name}}\" 已创建",
"createFailed": "创建故事失败",
"updateFailed": "更新故事失败",
"deleteFailed": "删除故事失败"
}
},
"storyContent": {
"selectStory": {
"title": "选择一个故事",
"hint": "从列表中选择一个故事以查看其内容"
},
"loading": "加载故事中…",
"notFound": {
"title": "未找到故事",
"hint": "无法加载所选故事"
},
"generatingCount_one": "生成 {{count}} 个音频中",
"generatingCount_other": "生成 {{count}} 个音频中",
"add": "添加",
"searchPlaceholder": "按名称或文字内容搜索……",
"searchNoMatches": "未找到匹配的生成",
"searchNoAvailable": "暂无可用的生成",
"exportAudio": "导出音频",
"empty": {
"title": "此故事暂无项目",
"hint": "使用下方输入框生成语音以添加项目"
},
"toast": {
"removeFailed": "移除项目失败",
"reorderFailed": "重新排序项目失败",
"exportFailed": "导出音频失败",
"addFailed": "添加生成失败"
}
},
"history": { "history": {
"deleteDialog": { "deleteDialog": {
"title": "删除生成", "title": "删除生成",