mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
feat(i18n): localize Audio Channels tab (list, dialogs, device picker)
Covers the "Audio Channels" title and "New Channel" button, the empty state, per-channel section labels (Output Devices / Assigned Voices), the Available Devices right pane with its three contextual hints, the "No voices assigned" fallback, and both Create/Edit dialogs (titles, descriptions, field labels, Select placeholders, and the "(default)" badge). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0c51132a93
commit
6e13a30a59
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Check, CheckCircle2, Edit, Plus, Speaker, Trash2 } from 'lucide-react';
|
import { Check, CheckCircle2, Edit, Plus, Speaker, Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ interface AudioDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AudioTab() {
|
export function AudioTab() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
const [editingChannel, setEditingChannel] = useState<string | null>(null);
|
const [editingChannel, setEditingChannel] = useState<string | null>(null);
|
||||||
@@ -119,14 +121,14 @@ export function AudioTab() {
|
|||||||
if (channelsLoading || devicesLoading) {
|
if (channelsLoading || devicesLoading) {
|
||||||
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...</div>
|
<div className="text-muted-foreground">{t('audioChannels.loading')}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChannelDelete = async (e: React.MouseEvent, channelId: string) => {
|
const handleChannelDelete = async (e: React.MouseEvent, channelId: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (await confirm('Delete this channel?')) {
|
if (await confirm(t('audioChannels.confirmDelete'))) {
|
||||||
deleteChannel.mutate(channelId);
|
deleteChannel.mutate(channelId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -140,10 +142,10 @@ export function AudioTab() {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-6 shrink-0">
|
<div className="flex items-center justify-between mb-6 shrink-0">
|
||||||
<h2 className="text-2xl font-bold">Audio Channels</h2>
|
<h2 className="text-2xl font-bold">{t('audioChannels.title')}</h2>
|
||||||
<Button onClick={() => setCreateDialogOpen(true)}>
|
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
New Channel
|
{t('audioChannels.newChannel')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,13 +160,10 @@ export function AudioTab() {
|
|||||||
{allChannels.length === 0 ? (
|
{allChannels.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md">
|
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md">
|
||||||
<Speaker className="h-12 w-12 text-muted-foreground mb-4" />
|
<Speaker className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">{t('audioChannels.empty.message')}</p>
|
||||||
No audio channels yet. Create your first channel to route voices to specific
|
|
||||||
devices.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => setCreateDialogOpen(true)}>
|
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Create Channel
|
{t('audioChannels.empty.action')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -195,7 +194,7 @@ export function AudioTab() {
|
|||||||
<div className="space-y-2.5 ml-10">
|
<div className="space-y-2.5 ml-10">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-1">
|
<div className="text-xs font-medium text-muted-foreground mb-1">
|
||||||
Output Devices
|
{t('audioChannels.labels.outputDevices')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{channel.device_ids.length > 0
|
{channel.device_ids.length > 0
|
||||||
@@ -224,7 +223,7 @@ export function AudioTab() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-1">
|
<div className="text-xs font-medium text-muted-foreground mb-1">
|
||||||
Assigned Voices
|
{t('audioChannels.labels.assignedVoices')}
|
||||||
</div>
|
</div>
|
||||||
<ChannelVoicesList channelId={channel.id} />
|
<ChannelVoicesList channelId={channel.id} />
|
||||||
</div>
|
</div>
|
||||||
@@ -270,13 +269,13 @@ export function AudioTab() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="shrink-0 mb-4">
|
<div className="shrink-0 mb-4">
|
||||||
<h3 className="text-lg font-semibold">Available Devices</h3>
|
<h3 className="text-lg font-semibold">{t('audioChannels.devices.title')}</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{selectedChannelId
|
{selectedChannelId
|
||||||
? selectedChannel?.is_default
|
? selectedChannel?.is_default
|
||||||
? 'Default channel uses system default device'
|
? t('audioChannels.devices.defaultNote')
|
||||||
: 'Click devices to add or remove them from the selected channel'
|
: t('audioChannels.devices.toggleHint')
|
||||||
: 'Select a channel to assign devices'}
|
: t('audioChannels.devices.selectHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{allDevices.length > 0 ? (
|
{allDevices.length > 0 ? (
|
||||||
@@ -344,8 +343,8 @@ export function AudioTab() {
|
|||||||
<CheckCircle2 className="h-12 w-12 text-muted-foreground mb-4" />
|
<CheckCircle2 className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<p className="text-muted-foreground text-center">
|
<p className="text-muted-foreground text-center">
|
||||||
{platform.metadata.isTauri
|
{platform.metadata.isTauri
|
||||||
? 'No audio devices found'
|
? t('audioChannels.devices.empty')
|
||||||
: 'Audio device selection requires Tauri'}
|
: t('audioChannels.devices.requiresTauri')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -394,6 +393,7 @@ export function AudioTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChannelVoicesList({ channelId }: { channelId: string }) {
|
function ChannelVoicesList({ channelId }: { channelId: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { data: voices } = useQuery({
|
const { data: voices } = useQuery({
|
||||||
queryKey: ['channel-voices', channelId],
|
queryKey: ['channel-voices', channelId],
|
||||||
queryFn: () => apiClient.getChannelVoices(channelId),
|
queryFn: () => apiClient.getChannelVoices(channelId),
|
||||||
@@ -416,7 +416,7 @@ function ChannelVoicesList({ channelId }: { channelId: string }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-muted-foreground">No voices assigned</span>
|
<span className="text-sm text-muted-foreground">{t('audioChannels.noVoicesAssigned')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -430,6 +430,7 @@ interface CreateChannelDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateChannelDialogProps) {
|
function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateChannelDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||||
|
|
||||||
@@ -445,23 +446,21 @@ function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateCh
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Audio Channel</DialogTitle>
|
<DialogTitle>{t('audioChannels.createDialog.title')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{t('audioChannels.createDialog.description')}</DialogDescription>
|
||||||
Create a new audio channel (bus) to route voices to specific output devices.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="channel-name">Channel Name</Label>
|
<Label htmlFor="channel-name">{t('audioChannels.fields.name')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="channel-name"
|
id="channel-name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="e.g., Virtual Cable, Broadcast"
|
placeholder={t('audioChannels.fields.namePlaceholder')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Output Devices</Label>
|
<Label>{t('audioChannels.labels.outputDevices')}</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedDevices[0] || ''}
|
value={selectedDevices[0] || ''}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -471,12 +470,12 @@ function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateCh
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select device" />
|
<SelectValue placeholder={t('audioChannels.selectDevice')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<SelectItem key={device.id} value={device.id}>
|
<SelectItem key={device.id} value={device.id}>
|
||||||
{device.name} {device.is_default && '(default)'}
|
{device.name} {device.is_default && `(${t('audioChannels.defaultSuffix')})`}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -509,10 +508,10 @@ function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateCh
|
|||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={!name.trim()}>
|
<Button onClick={handleSubmit} disabled={!name.trim()}>
|
||||||
Create
|
{t('audioChannels.createDialog.action')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -545,6 +544,7 @@ function EditChannelDialog({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onSetVoices,
|
onSetVoices,
|
||||||
}: EditChannelDialogProps) {
|
}: EditChannelDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [name, setName] = useState(channel.name);
|
const [name, setName] = useState(channel.name);
|
||||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(channel.device_ids);
|
const [selectedDevices, setSelectedDevices] = useState<string[]>(channel.device_ids);
|
||||||
const [selectedVoices, setSelectedVoices] = useState<string[]>(channelVoices);
|
const [selectedVoices, setSelectedVoices] = useState<string[]>(channelVoices);
|
||||||
@@ -560,16 +560,16 @@ function EditChannelDialog({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit Channel</DialogTitle>
|
<DialogTitle>{t('audioChannels.editDialog.title')}</DialogTitle>
|
||||||
<DialogDescription>Update channel settings and voice assignments.</DialogDescription>
|
<DialogDescription>{t('audioChannels.editDialog.description')}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="edit-channel-name">Channel Name</Label>
|
<Label htmlFor="edit-channel-name">{t('audioChannels.fields.name')}</Label>
|
||||||
<Input id="edit-channel-name" value={name} onChange={(e) => setName(e.target.value)} />
|
<Input id="edit-channel-name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Output Devices</Label>
|
<Label>{t('audioChannels.labels.outputDevices')}</Label>
|
||||||
<Select
|
<Select
|
||||||
value=""
|
value=""
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -579,12 +579,12 @@ function EditChannelDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Add device" />
|
<SelectValue placeholder={t('audioChannels.addDevice')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<SelectItem key={device.id} value={device.id}>
|
<SelectItem key={device.id} value={device.id}>
|
||||||
{device.name} {device.is_default && '(default)'}
|
{device.name} {device.is_default && `(${t('audioChannels.defaultSuffix')})`}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -615,7 +615,7 @@ function EditChannelDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Assigned Voices</Label>
|
<Label>{t('audioChannels.labels.assignedVoices')}</Label>
|
||||||
<Select
|
<Select
|
||||||
value=""
|
value=""
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -625,7 +625,7 @@ function EditChannelDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Add voice" />
|
<SelectValue placeholder={t('audioChannels.addVoice')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{profiles.map((profile) => (
|
{profiles.map((profile) => (
|
||||||
@@ -663,10 +663,10 @@ function EditChannelDialog({
|
|||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={!name.trim()}>
|
<Button onClick={handleSubmit} disabled={!name.trim()}>
|
||||||
Save
|
{t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -21,6 +21,46 @@
|
|||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"updateBadge": "Update"
|
"updateBadge": "Update"
|
||||||
},
|
},
|
||||||
|
"audioChannels": {
|
||||||
|
"title": "Audio Channels",
|
||||||
|
"newChannel": "New Channel",
|
||||||
|
"loading": "Loading…",
|
||||||
|
"confirmDelete": "Delete this channel?",
|
||||||
|
"noVoicesAssigned": "No voices assigned",
|
||||||
|
"selectDevice": "Select device",
|
||||||
|
"addDevice": "Add device",
|
||||||
|
"addVoice": "Add voice",
|
||||||
|
"defaultSuffix": "default",
|
||||||
|
"empty": {
|
||||||
|
"message": "No audio channels yet. Create your first channel to route voices to specific devices.",
|
||||||
|
"action": "Create Channel"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"outputDevices": "Output Devices",
|
||||||
|
"assignedVoices": "Assigned Voices"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Available Devices",
|
||||||
|
"defaultNote": "Default channel uses system default device",
|
||||||
|
"toggleHint": "Click devices to add or remove them from the selected channel",
|
||||||
|
"selectHint": "Select a channel to assign devices",
|
||||||
|
"empty": "No audio devices found",
|
||||||
|
"requiresTauri": "Audio device selection requires Tauri"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"name": "Channel Name",
|
||||||
|
"namePlaceholder": "e.g., Virtual Cable, Broadcast"
|
||||||
|
},
|
||||||
|
"createDialog": {
|
||||||
|
"title": "Create Audio Channel",
|
||||||
|
"description": "Create a new audio channel (bus) to route voices to specific output devices.",
|
||||||
|
"action": "Create"
|
||||||
|
},
|
||||||
|
"editDialog": {
|
||||||
|
"title": "Edit Channel",
|
||||||
|
"description": "Update channel settings and voice assignments."
|
||||||
|
}
|
||||||
|
},
|
||||||
"profileForm": {
|
"profileForm": {
|
||||||
"createTitle": "Create Voice",
|
"createTitle": "Create Voice",
|
||||||
"editTitle": "Edit Voice",
|
"editTitle": "Edit Voice",
|
||||||
|
|||||||
@@ -21,6 +21,46 @@
|
|||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"updateBadge": "更新"
|
"updateBadge": "更新"
|
||||||
},
|
},
|
||||||
|
"audioChannels": {
|
||||||
|
"title": "音频通道",
|
||||||
|
"newChannel": "新建通道",
|
||||||
|
"loading": "加载中…",
|
||||||
|
"confirmDelete": "删除此通道?",
|
||||||
|
"noVoicesAssigned": "未分配声音",
|
||||||
|
"selectDevice": "选择设备",
|
||||||
|
"addDevice": "添加设备",
|
||||||
|
"addVoice": "添加声音",
|
||||||
|
"defaultSuffix": "默认",
|
||||||
|
"empty": {
|
||||||
|
"message": "暂无音频通道。创建您的第一个通道,将声音路由到特定设备。",
|
||||||
|
"action": "创建通道"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"outputDevices": "输出设备",
|
||||||
|
"assignedVoices": "已分配声音"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "可用设备",
|
||||||
|
"defaultNote": "默认通道使用系统默认设备",
|
||||||
|
"toggleHint": "点击设备以将其添加到或从选定通道中移除",
|
||||||
|
"selectHint": "选择通道以分配设备",
|
||||||
|
"empty": "未找到音频设备",
|
||||||
|
"requiresTauri": "音频设备选择需要 Tauri"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"name": "通道名称",
|
||||||
|
"namePlaceholder": "例如:虚拟线缆、广播"
|
||||||
|
},
|
||||||
|
"createDialog": {
|
||||||
|
"title": "创建音频通道",
|
||||||
|
"description": "创建新的音频通道(总线),将声音路由到特定的输出设备。",
|
||||||
|
"action": "创建"
|
||||||
|
},
|
||||||
|
"editDialog": {
|
||||||
|
"title": "编辑通道",
|
||||||
|
"description": "更新通道设置和声音分配。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"profileForm": {
|
"profileForm": {
|
||||||
"createTitle": "创建声音",
|
"createTitle": "创建声音",
|
||||||
"editTitle": "编辑声音",
|
"editTitle": "编辑声音",
|
||||||
|
|||||||
Reference in New Issue
Block a user