feat: added translations for 4 tabs in Settings screen

This commit is contained in:
Xiaohan-Tian
2026-05-31 00:21:44 -07:00
parent 9fb206c456
commit 59dea65b0d
10 changed files with 415 additions and 113 deletions
@@ -2,6 +2,7 @@ import React from 'react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import AudioIOSettings from './AudioIOSettings';
import { translate } from '../../../i18n/translate';
const configState = new Map<string, unknown>([
['audio.input_device_id', 'default'],
@@ -63,16 +64,16 @@ describe('AudioIOSettings', () => {
it('renders input/output selectors and refresh action', async () => {
render(<AudioIOSettings />);
expect(await screen.findByText('Audio I/O')).toBeTruthy();
expect(screen.getByLabelText('Audio Input Device')).toBeTruthy();
expect(screen.getByLabelText('Audio Output Device')).toBeTruthy();
expect(screen.getByText('Refresh Device List')).toBeTruthy();
expect(await screen.findByText(translate('settings.audioIo.title', undefined, 'en_us'))).toBeTruthy();
expect(screen.getByLabelText(translate('settings.audioIo.inputDevice', undefined, 'en_us'))).toBeTruthy();
expect(screen.getByLabelText(translate('settings.audioIo.outputDevice', undefined, 'en_us'))).toBeTruthy();
expect(screen.getByText(translate('settings.audioIo.refresh', undefined, 'en_us'))).toBeTruthy();
});
it('persists input device changes', async () => {
render(<AudioIOSettings />);
const select = await screen.findByLabelText('Audio Input Device');
const select = await screen.findByLabelText(translate('settings.audioIo.inputDevice', undefined, 'en_us'));
fireEvent.change(select, { target: { value: 'mic-1' } });
await waitFor(() => {
@@ -7,8 +7,10 @@ import {
supportsAudioContextSinkSelection,
type AudioDeviceOption,
} from '../../../util/audioDeviceUtil';
import { useI18n } from '../../../i18n/useI18n';
const AudioIOSettings: React.FC = () => {
const { t } = useI18n();
const [inputDeviceId, setInputDeviceId] = useState<string>('default');
const [outputDeviceId, setOutputDeviceId] = useState<string>('default');
const [inputs, setInputs] = useState<AudioDeviceOption[]>([getDefaultAudioDeviceOption('audioinput')]);
@@ -67,7 +69,7 @@ const AudioIOSettings: React.FC = () => {
...snapshot.inputs,
{
deviceId: configuredInputId,
label: 'Previously Selected Input (permission required to verify)',
label: t('settings.audioIo.previousSelectedInput'),
kind: 'audioinput' as const,
isDefault: false,
},
@@ -78,7 +80,7 @@ const AudioIOSettings: React.FC = () => {
...snapshot.outputs,
{
deviceId: configuredOutputId,
label: 'Previously Selected Output (permission required to verify)',
label: t('settings.audioIo.previousSelectedOutput'),
kind: 'audiooutput' as const,
isDefault: false,
},
@@ -91,7 +93,7 @@ const AudioIOSettings: React.FC = () => {
if (!hasInput && configuredInputId !== 'default' && snapshot.labelsAvailable) {
setInputDeviceId('default');
await configManager.set('audio.input_device_id', 'default');
setDeviceStatus('Previously selected audio input device is unavailable; using System Default.');
setDeviceStatus(t('settings.audioIo.status.inputUnavailable'));
} else {
setInputDeviceId(hasInput || !snapshot.labelsAvailable ? configuredInputId : 'default');
}
@@ -99,17 +101,17 @@ const AudioIOSettings: React.FC = () => {
if (!hasOutput && configuredOutputId !== 'default' && snapshot.labelsAvailable) {
setOutputDeviceId('default');
await configManager.set('audio.output_device_id', 'default');
setDeviceStatus('Previously selected audio output device is unavailable; using System Default.');
setDeviceStatus(t('settings.audioIo.status.outputUnavailable'));
} else {
setOutputDeviceId(hasOutput || !snapshot.labelsAvailable ? configuredOutputId : 'default');
}
if (showStatus) {
setDeviceStatus(current => current || 'Audio device list refreshed.');
setDeviceStatus(current => current || t('settings.audioIo.status.refreshed'));
}
} catch (error) {
console.error('Unable to refresh audio devices:', error);
setDeviceStatus('Unable to read audio devices from the browser.');
setDeviceStatus(t('settings.audioIo.status.readFailed'));
} finally {
setRefreshing(false);
}
@@ -119,63 +121,63 @@ const AudioIOSettings: React.FC = () => {
setInputDeviceId(value);
await configManager.set('audio.input_device_id', value);
setDeviceStatus(value === 'default'
? 'Audio input will use System Default on the next recording session.'
: 'Audio input will change on the next recording session.');
? t('settings.audioIo.status.inputDefaultNextSession')
: t('settings.audioIo.status.inputChangedNextSession'));
};
const handleOutputDeviceChange = async (value: string) => {
setOutputDeviceId(value);
await configManager.set('audio.output_device_id', value);
setDeviceStatus(value === 'default'
? 'Audio output will use System Default after refresh.'
: 'Audio output device saved. Refresh the page to apply it in v1.');
? t('settings.audioIo.status.outputDefaultAfterRefresh')
: t('settings.audioIo.status.outputSavedRefreshRequired'));
};
const handleChooseOutputDevice = async () => {
try {
const selectedDevice = await promptForAudioOutputDevice();
if (!selectedDevice) {
setDeviceStatus('This browser does not support prompting for audio output devices.');
setDeviceStatus(t('settings.audioIo.status.outputPromptUnsupported'));
return;
}
setOutputDeviceId(selectedDevice.deviceId);
await configManager.set('audio.output_device_id', selectedDevice.deviceId);
await refreshDevices(false);
setDeviceStatus('Output device selected. Refresh the page to apply it in v1.');
setDeviceStatus(t('settings.audioIo.status.outputSelectedRefreshRequired'));
} catch (error) {
console.error('Unable to choose audio output device:', error);
setDeviceStatus('The browser did not allow selecting a non-default output device.');
setDeviceStatus(t('settings.audioIo.status.outputSelectionDenied'));
}
};
return (
<div className="settings-section">
<div className="settings-section-header">
<h3>Audio I/O</h3>
<h3>{t('settings.audioIo.title')}</h3>
</div>
<div className="settings-section-content">
<div className="settings-group">
<div className="settings-group-header">
<h4>Device Routing</h4>
<h4>{t('settings.audioIo.deviceRouting')}</h4>
<button
type="button"
className="settings-btn"
onClick={() => void refreshDevices()}
disabled={refreshing}
>
{refreshing ? 'Refreshing' : 'Refresh Device List'}
{refreshing ? t('settings.audioIo.refreshing') : t('settings.audioIo.refresh')}
</button>
</div>
<div className="settings-description">
Choose the devices KGStudio should use for recording and playback. Input changes apply to the next recording session. Output changes require a page refresh in v1.
{t('settings.audioIo.description')}
</div>
<div className="settings-item">
<label className="settings-label" htmlFor="audio-input-device-select">
Audio Input Device
{t('settings.audioIo.inputDevice')}
</label>
<select
id="audio-input-device-select"
@@ -191,13 +193,13 @@ const AudioIOSettings: React.FC = () => {
))}
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Changes apply to the next audio recording. If the selected device is removed, KGStudio will fall back to System Default.
{t('settings.audioIo.inputHelp')}
</div>
</div>
<div className="settings-item">
<label className="settings-label" htmlFor="audio-output-device-select">
Audio Output Device
{t('settings.audioIo.outputDevice')}
</label>
<select
id="audio-output-device-select"
@@ -215,16 +217,16 @@ const AudioIOSettings: React.FC = () => {
{supportsOutputPrompt && (
<div style={{ marginTop: '10px' }}>
<button type="button" className="settings-btn" onClick={() => void handleChooseOutputDevice()}>
Choose Output Device
{t('settings.audioIo.chooseOutputDevice')}
</button>
</div>
)}
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Output-device changes require refresh in v1. Browser support for non-default output routing is limited, so KGStudio will continue on System Default when unsupported.
{t('settings.audioIo.outputHelp')}
</div>
{!supportsOutputSink && (
<div className="settings-help" style={{ fontSize: '12px', color: '#d0a56b', marginTop: '6px' }}>
This browser does not expose reliable live Web Audio sink switching. Non-default output selection is best-effort and may remain on System Default.
{t('settings.audioIo.outputSinkUnsupported')}
</div>
)}
</div>
@@ -5,8 +5,10 @@ import {
normalizeSpectrogramHeightResolution,
type SpectrogramHeightResolution,
} from '../../../util/spectrogramUtil';
import { useI18n } from '../../../i18n/useI18n';
const BehaviorSettings: React.FC = () => {
const { t } = useI18n();
const [playheadUpdateFrequency, setPlayheadUpdateFrequency] = useState<number>(30);
const [spectrogramHeightResolution, setSpectrogramHeightResolution] = useState<SpectrogramHeightResolution>(3);
const [chatboxDefaultOpen, setChatboxDefaultOpen] = useState<boolean>(true);
@@ -78,11 +80,11 @@ const BehaviorSettings: React.FC = () => {
// Validate the input
if (isNaN(numValueMs)) {
errors.push('Lookahead time must be a valid number');
errors.push(t('settings.behavior.validation.lookahead.invalidNumber'));
} else if (numValueSeconds < 0) {
errors.push('Lookahead time must be between 0 and 0.5 seconds (0-500ms)');
errors.push(t('settings.behavior.validation.lookahead.range'));
} else if (numValueSeconds > 0.5) {
errors.push('Lookahead time must be between 0 and 0.5 seconds (0-500ms)');
errors.push(t('settings.behavior.validation.lookahead.range'));
}
setLookaheadValidationErrors(errors);
@@ -108,11 +110,11 @@ const BehaviorSettings: React.FC = () => {
// Validate the input
if (isNaN(numValueMs)) {
errors.push('Playback delay must be a valid number');
errors.push(t('settings.behavior.validation.playbackDelay.invalidNumber'));
} else if (numValueSeconds < 0) {
errors.push('Playback delay must be between 0 and 0.5 seconds (0-500ms)');
errors.push(t('settings.behavior.validation.playbackDelay.range'));
} else if (numValueSeconds > 0.5) {
errors.push('Playback delay must be between 0 and 0.5 seconds (0-500ms)');
errors.push(t('settings.behavior.validation.playbackDelay.range'));
}
setPlaybackDelayValidationErrors(errors);
@@ -138,11 +140,11 @@ const BehaviorSettings: React.FC = () => {
const errors: string[] = [];
if (isNaN(numValueMs)) {
errors.push('MIDI input latency must be a valid number');
errors.push(t('settings.behavior.validation.recordingOffset.invalidNumber'));
} else if (numValueSeconds < 0) {
errors.push('MIDI input latency must be between 0 and 0.5 seconds (0-500ms)');
errors.push(t('settings.behavior.validation.recordingOffset.range'));
} else if (numValueSeconds > 0.5) {
errors.push('MIDI input latency must be between 0 and 0.5 seconds (0-500ms)');
errors.push(t('settings.behavior.validation.recordingOffset.range'));
}
setRecordingOffsetValidationErrors(errors);
@@ -169,16 +171,16 @@ const BehaviorSettings: React.FC = () => {
return (
<div className="settings-section">
<div className="settings-section-header">
<h3>Behavior</h3>
<h3>{t('settings.behavior.title')}</h3>
</div>
<div className="settings-section-content">
<div className="settings-group">
<h4>Editor</h4>
<h4>{t('settings.behavior.editor')}</h4>
<div className="settings-item">
<label className="settings-label">
Playhead Update Frequency (fps)
{t('settings.behavior.playheadUpdateFrequency')}
</label>
<select
className="settings-select"
@@ -190,13 +192,13 @@ const BehaviorSettings: React.FC = () => {
<option value="60">60</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Update frequency for the playhead animation during playback. Higher values (60 fps) provide smoother animation but use more CPU. Lower values (10 fps) are more efficient. Changes apply immediately without restart.
{t('settings.behavior.playheadUpdateFrequencyHelp')}
</div>
</div>
<div className="settings-item">
<label className="settings-label">
Spectrogram Height Resolution
{t('settings.behavior.spectrogramHeightResolution')}
</label>
<select
className="settings-select"
@@ -208,35 +210,35 @@ const BehaviorSettings: React.FC = () => {
<option value="5">5x</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Controls vertical spectrogram detail across the visible pitch range. Higher values sharpen pitch contours but use more CPU and memory while computing the spectrogram.
{t('settings.behavior.spectrogramHeightResolutionHelp')}
</div>
</div>
</div>
<div className="settings-group">
<h4>Chat Box</h4>
<h4>{t('settings.behavior.chatBox')}</h4>
<div className="settings-item">
<label className="settings-label">
Open at Start Up
{t('settings.behavior.openAtStartup')}
</label>
<select
className="settings-select"
value={chatboxDefaultOpen ? 'yes' : 'no'}
onChange={(e) => handleChatboxDefaultOpenChange(e.target.value)}
>
<option value="no">No</option>
<option value="yes">Yes</option>
<option value="no">{t('settings.no')}</option>
<option value="yes">{t('settings.yes')}</option>
</select>
</div>
</div>
<div className="settings-group">
<h4>Audio</h4>
<h4>{t('settings.behavior.audio')}</h4>
<div className="settings-item">
<label className="settings-label">
Lookahead Time (ms)
{t('settings.behavior.lookaheadTime')}
</label>
<input
type="number"
@@ -257,13 +259,13 @@ const BehaviorSettings: React.FC = () => {
</div>
)}
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Audio scheduling lookahead time (0-500ms). Lower values (10-20ms) reduce MIDI input latency but may cause audio glitches on slower systems. Higher values (100ms+) are better for playback stability. Changes apply immediately without restart.
{t('settings.behavior.lookaheadTimeHelp')}
</div>
</div>
<div className="settings-item">
<label className="settings-label">
Playback Delay (ms)
{t('settings.behavior.playbackDelay')}
</label>
<input
type="number"
@@ -284,31 +286,31 @@ const BehaviorSettings: React.FC = () => {
</div>
)}
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Playback will start with a short delay after pressing the start button (0-500ms). Increasing this value might help stabilize playback, especially for the first few ticks if the lookahead value is too low. Changes apply immediately without restart.
{t('settings.behavior.playbackDelayHelp')}
</div>
</div>
<div className="settings-item">
<label className="settings-label">
MIDI Automation Interpolation
{t('settings.behavior.midiAutomationInterpolation')}
</label>
<select
className="settings-select"
value={midiAutomationInterpolationIntervalMs}
onChange={(e) => handleMidiAutomationInterpolationIntervalChange(e.target.value)}
>
<option value="20">Low-end (20 ms)</option>
<option value="10">Balanced (10 ms)</option>
<option value="5">High quality (5 ms)</option>
<option value="20">{t('settings.behavior.midiAutomationInterpolation.low')}</option>
<option value="10">{t('settings.behavior.midiAutomationInterpolation.balanced')}</option>
<option value="5">{t('settings.behavior.midiAutomationInterpolation.high')}</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Controls how densely MIDI automation are baked for playback and bounce. Smaller intervals sound smoother but schedule more events.
{t('settings.behavior.midiAutomationInterpolationHelp')}
</div>
</div>
<div className="settings-item">
<label className="settings-label">
MIDI Input Latency (ms)
{t('settings.behavior.midiInputLatency')}
</label>
<input
type="number"
@@ -329,43 +331,43 @@ const BehaviorSettings: React.FC = () => {
</div>
)}
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Timing correction for MIDI recording (0-500ms). If recorded notes appear slightly late compared to where you intended to play them, increase this value to match your MIDI device's input latency. Each note's position is shifted back by this amount when committed.
{t('settings.behavior.midiInputLatencyHelp')}
</div>
</div>
<div className="settings-item">
<label className="settings-label">
Bounce Starts From Beat 1
{t('settings.behavior.bounceStartsFromBeat1')}
</label>
<select
className="settings-select"
value={bounceStartsFromBeat1 ? 'yes' : 'no'}
onChange={(e) => handleBounceStartsFromBeat1Change(e.target.value)}
>
<option value="no">No</option>
<option value="yes">Yes</option>
<option value="no">{t('settings.no')}</option>
<option value="yes">{t('settings.yes')}</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Yes includes leading silence from the start of the song up to the first rendered region when bouncing WAV/MP3. No trims that leading silence and starts bounce at the first rendered note or audio region.
{t('settings.behavior.bounceStartsFromBeat1Help')}
</div>
</div>
<div className="settings-item">
<label className="settings-label">
Capture Audio for Screen Sharing
{t('settings.behavior.captureAudioForScreenSharing')}
</label>
<select
className="settings-select"
value={enableAudioCapture ? 'yes' : 'no'}
onChange={(e) => handleEnableAudioCaptureChange(e.target.value)}
>
<option value="no">No</option>
<option value="yes">Yes</option>
<option value="no">{t('settings.no')}</option>
<option value="yes">{t('settings.yes')}</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Restart KGStudio (refresh the page) to take effect. Enable this option when KGStudio's audio cannot be captured during screen sharing in video calls (e.g., Zoom, Teams). This creates an additional audio stream that screen capture applications can detect.
{t('settings.behavior.captureAudioForScreenSharingHelp')}
<br />
<b>It is important to make sure when screen sharing in Zoom, the "Share Sound" option is enabled.</b>
<b>{t('settings.behavior.captureAudioForScreenSharingWarning')}</b>
</div>
</div>
</div>
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import ChordGuideSettings from './ChordGuideSettings';
import chordGuideDataJson from '../../../../public/resources/modes/chord_guide.json';
import type { ChordGuideData } from '../../../core/ChordGuideTypes';
import { translate } from '../../../i18n/translate';
const chordGuideData = chordGuideDataJson as ChordGuideData;
@@ -41,7 +42,9 @@ vi.mock('../../../core/KGCore', () => ({
}));
describe('ChordGuideSettings', () => {
const getGroup = (heading: 'Major Candidate Chords' | 'Minor Candidate Chords') => (
const majorHeading = translate('settings.chordGuide.group.major', undefined, 'en_us');
const minorHeading = translate('settings.chordGuide.group.minor', undefined, 'en_us');
const getGroup = (heading: string) => (
screen.getByText(heading).closest('.settings-group') as HTMLElement
);
@@ -67,17 +70,19 @@ describe('ChordGuideSettings', () => {
it('renders major and minor groups with base-key guidance', async () => {
render(<ChordGuideSettings />);
expect(await screen.findByText('Major Candidate Chords')).toBeTruthy();
expect(screen.getByText('Minor Candidate Chords')).toBeTruthy();
expect(screen.getByText(/relative to C major/i)).toBeTruthy();
expect(screen.getByText(/relative to A minor/i)).toBeTruthy();
expect(await screen.findByText(majorHeading)).toBeTruthy();
expect(screen.getByText(minorHeading)).toBeTruthy();
expect(screen.getByText(translate('settings.chordGuide.help.major', undefined, 'en_us'))).toBeTruthy();
expect(screen.getByText(translate('settings.chordGuide.help.minor', undefined, 'en_us'))).toBeTruthy();
expect(screen.getByText(translate('settings.chordGuide.resetToDefault', undefined, 'en_us'))).toBeTruthy();
expect(screen.getAllByRole('columnheader', { name: translate('settings.chordGuide.table.chord', undefined, 'en_us') })).toHaveLength(2);
});
it('switches T/S/D tabs as a mutex control', async () => {
render(<ChordGuideSettings />);
await screen.findByText('Major Candidate Chords');
const majorGroup = getGroup('Major Candidate Chords');
await screen.findByText(majorHeading);
const majorGroup = getGroup(majorHeading);
expect(within(majorGroup).getByTitle('C')).toBeTruthy();
fireEvent.click(within(majorGroup).getByRole('button', { name: 'S' }));
@@ -89,8 +94,8 @@ describe('ChordGuideSettings', () => {
it('adds and deletes rows in the active table', async () => {
render(<ChordGuideSettings />);
await screen.findByText('Major Candidate Chords');
const addButtons = screen.getAllByTitle(/Add .* chord/);
await screen.findByText(majorHeading);
const addButtons = screen.getAllByTitle(translate('settings.chordGuide.addTitle', { group: majorHeading }, 'en_us'));
fireEvent.click(addButtons[0]);
await waitFor(() => {
@@ -106,7 +111,7 @@ describe('ChordGuideSettings', () => {
const rows = screen.getAllByRole('row');
fireEvent.click(rows[1]);
const deleteButtons = screen.getAllByTitle('Delete selected rows');
const deleteButtons = screen.getAllByTitle(translate('settings.chordGuide.deleteSelectedRows', undefined, 'en_us'));
fireEvent.click(deleteButtons[0]);
await waitFor(() => {
@@ -117,8 +122,8 @@ describe('ChordGuideSettings', () => {
it('updates notes and source after a valid chord edit', async () => {
render(<ChordGuideSettings />);
await screen.findByText('Major Candidate Chords');
const majorGroup = getGroup('Major Candidate Chords');
await screen.findByText(majorHeading);
const majorGroup = getGroup(majorHeading);
fireEvent.click(within(majorGroup).getByRole('button', { name: 'S' }));
const chordCell = within(majorGroup).getByTitle('F');
@@ -142,7 +147,7 @@ describe('ChordGuideSettings', () => {
it('rejects invalid chord edits', async () => {
render(<ChordGuideSettings />);
await screen.findByText('Major Candidate Chords');
await screen.findByText(majorHeading);
const chordCell = screen.getAllByTitle('C')[0];
fireEvent.doubleClick(chordCell);
const input = screen.getByDisplayValue('C');
@@ -150,14 +155,14 @@ describe('ChordGuideSettings', () => {
fireEvent.keyDown(input, { key: 'Enter' });
await waitFor(() => {
expect(showAlertMock).toHaveBeenCalledWith(expect.stringContaining('valid chord symbol'));
expect(showAlertMock).toHaveBeenCalledWith(translate('settings.chordGuide.invalidChordSymbol', undefined, 'en_us'));
});
});
it('enforces the 128-character note limit', async () => {
render(<ChordGuideSettings />);
await screen.findByText('Major Candidate Chords');
await screen.findByText(majorHeading);
const noteCell = screen.getAllByRole('cell').find((cell) => cell.textContent === chordGuideData.ionian.T[0].note) as HTMLElement;
fireEvent.doubleClick(noteCell);
const input = screen.getByDisplayValue(chordGuideData.ionian.T[0].note);
@@ -194,7 +199,7 @@ describe('ChordGuideSettings', () => {
render(<ChordGuideSettings />);
expect((await screen.findAllByTitle('G')).length).toBeGreaterThan(0);
fireEvent.click(screen.getByText('Reset to Default'));
fireEvent.click(screen.getByText(translate('settings.chordGuide.resetToDefault', undefined, 'en_us')));
await waitFor(() => {
expect(configManagerMock.set).toHaveBeenCalledWith('chord_guide.custom_items', null);
@@ -19,6 +19,7 @@ import {
import { parseChordSymbol } from '../../../util/chordUtil';
import { showAlert } from '../../../util/dialogUtil';
import { isModifierKeyPressed } from '../../../util/osUtil';
import { useI18n } from '../../../i18n/useI18n';
type FunctionType = 'T' | 'S' | 'D';
type EditableColumn = 'name' | 'note';
@@ -36,16 +37,6 @@ const FUNCTION_BUTTONS: Array<{ value: FunctionType; label: string; title: strin
{ value: 'D', label: 'D', title: 'Dominant' },
];
const GROUP_LABELS: Record<ChordGuideGroupKey, string> = {
major: 'Major Candidate Chords',
minor: 'Minor Candidate Chords',
};
const GROUP_HELP: Record<ChordGuideGroupKey, string> = {
major: 'Enter chords relative to C major. The app will transpose them automatically for other major key signatures.',
minor: 'Enter chords relative to A minor. The app will transpose them automatically for other minor key signatures.',
};
const CONFIG_KEY = 'chord_guide.custom_items';
function cloneModeDefinition(definition: ChordGuideModeDefinition): ChordGuideModeDefinition {
@@ -86,6 +77,7 @@ async function loadBundledChordGuideData(): Promise<ChordGuideData> {
}
const ChordGuideSettings: React.FC = () => {
const { t } = useI18n();
const configManager = ConfigManager.instance();
const [defaultsData, setDefaultsData] = React.useState<ChordGuideData | null>(null);
const [customConfig, setCustomConfig] = React.useState<ChordGuideCustomConfig | null>(null);
@@ -115,9 +107,9 @@ const ChordGuideSettings: React.FC = () => {
void initialize().catch((error) => {
console.error('Failed to initialize chord guide settings:', error);
void showAlert('Failed to load chord guide settings.');
void showAlert(t('settings.chordGuide.loadError'));
});
}, [configManager]);
}, [configManager, t]);
React.useEffect(() => {
if (!editingCell) {
@@ -180,12 +172,12 @@ const ChordGuideSettings: React.FC = () => {
const trimmedValue = editingCell.value.trim();
if (column === 'name') {
if (!trimmedValue || parseChordSymbol(trimmedValue) === null) {
await showAlert('Please enter a valid chord symbol. Example: Bm7b5');
await showAlert(t('settings.chordGuide.invalidChordSymbol'));
return;
}
const derived = buildDerivedChordGuideItem(group, { name: trimmedValue, note: row.note });
if (!derived) {
await showAlert('Unable to derive notes for this chord. Please use a supported chord symbol.');
await showAlert(t('settings.chordGuide.unsupportedChordSymbol'));
return;
}
await updateGroupRows(group, functionType, (rows) => rows.map((candidate, index) => (
@@ -290,14 +282,16 @@ const ChordGuideSettings: React.FC = () => {
const rows = getModeDefinition(customConfig, group)[functionType];
const selection = selectedRows[group];
const groupLabel = t(`settings.chordGuide.group.${group}`);
return (
<div className="settings-group" key={group}>
<div className="settings-group-header">
<h4>{GROUP_LABELS[group]}</h4>
<h4>{groupLabel}</h4>
</div>
<div className="settings-description">{GROUP_HELP[group]}</div>
<div className="settings-description">{t(`settings.chordGuide.help.${group}`)}</div>
<div className="event-list-tabs" role="tablist" aria-label={`${GROUP_LABELS[group]} functions`}>
<div className="event-list-tabs" role="tablist" aria-label={t('settings.chordGuide.functionsAria', { group: groupLabel })}>
{FUNCTION_BUTTONS.map((button) => (
<button
key={button.value}
@@ -319,7 +313,7 @@ const ChordGuideSettings: React.FC = () => {
<div className="event-list-toolbar-group">
<button
className="event-list-add-button settings-chord-guide-add-button"
title={`Add ${GROUP_LABELS[group]} chord`}
title={t('settings.chordGuide.addTitle', { group: groupLabel })}
type="button"
onClick={() => { void handleAddRow(group); }}
>
@@ -329,7 +323,7 @@ const ChordGuideSettings: React.FC = () => {
<div className="event-list-toolbar-group event-list-toolbar-group-right">
<button
className="event-list-delete-button"
title="Delete selected rows"
title={t('settings.chordGuide.deleteSelectedRows')}
type="button"
onClick={() => { void handleDeleteRows(group); }}
disabled={selection.size === 0}
@@ -343,10 +337,10 @@ const ChordGuideSettings: React.FC = () => {
<table className="event-list-table">
<thead>
<tr>
<th>Chord</th>
<th>Notes</th>
<th>Source</th>
<th>Note</th>
<th>{t('settings.chordGuide.table.chord')}</th>
<th>{t('settings.chordGuide.table.notes')}</th>
<th>{t('settings.chordGuide.table.source')}</th>
<th>{t('settings.chordGuide.table.note')}</th>
</tr>
</thead>
<tbody>
@@ -415,13 +409,13 @@ const ChordGuideSettings: React.FC = () => {
return (
<div className="settings-section">
<div className="settings-section-header">
<h3>Chord Guide</h3>
<h3>{t('settings.chordGuide.title')}</h3>
</div>
<div className="settings-section-content">
<div className="settings-help-links settings-chord-guide-reset-row">
<button className="settings-help" type="button" onClick={() => { void handleResetToDefault(); }}>
Reset to Default
{t('settings.chordGuide.resetToDefault')}
</button>
</div>
{renderGroup('major')}
@@ -1,7 +1,9 @@
import React, { useState, useEffect, useCallback } from 'react';
import { ConfigManager } from '../../../core/config/ConfigManager';
import { useI18n } from '../../../i18n/useI18n';
const TemplatesSettings: React.FC = () => {
const { t } = useI18n();
const [customInstructions, setCustomInstructions] = useState<string>('');
const configManager = ConfigManager.instance();
@@ -42,17 +44,17 @@ const TemplatesSettings: React.FC = () => {
return (
<div className="settings-section">
<div className="settings-section-header">
<h3>Templates</h3>
<h3>{t('settings.templates.title')}</h3>
</div>
<div className="settings-section-content">
<div className="settings-group">
<h4>Custom Instructions</h4>
<h4>{t('settings.templates.customInstructions')}</h4>
<div className="settings-item">
<textarea
className="settings-textarea"
placeholder="Please input your custom instructions for the K.G.Studio Musician Assistant"
placeholder={t('settings.templates.placeholder')}
rows={8}
value={customInstructions}
onChange={(e) => handleCustomInstructionsChange(e.target.value)}
@@ -64,4 +66,4 @@ const TemplatesSettings: React.FC = () => {
);
};
export default TemplatesSettings;
export default TemplatesSettings;
+74
View File
@@ -74,6 +74,80 @@ export const enUsMessages: TranslationMessages = {
'settings.general.kgone.enabledOption': 'Enabled',
'settings.general.kgone.serverBaseUrl': 'Server Base URL',
'settings.general.kgone.serverBaseUrlHelp': 'Base URL of a running K.G.One Music Studio server. Used for full-song generation, clip generation, and stem separation.',
'settings.audioIo.title': 'Audio I/O',
'settings.audioIo.deviceRouting': 'Device Routing',
'settings.audioIo.refresh': 'Refresh Device List',
'settings.audioIo.refreshing': 'Refreshing…',
'settings.audioIo.description': 'Choose the devices KGStudio should use for recording and playback. Input changes apply to the next recording session. Output changes require a page refresh in v1.',
'settings.audioIo.inputDevice': 'Audio Input Device',
'settings.audioIo.outputDevice': 'Audio Output Device',
'settings.audioIo.inputHelp': 'Changes apply to the next audio recording. If the selected device is removed, KGStudio will fall back to System Default.',
'settings.audioIo.outputHelp': 'Output-device changes require refresh in v1. Browser support for non-default output routing is limited, so KGStudio will continue on System Default when unsupported.',
'settings.audioIo.outputSinkUnsupported': 'This browser does not expose reliable live Web Audio sink switching. Non-default output selection is best-effort and may remain on System Default.',
'settings.audioIo.chooseOutputDevice': 'Choose Output Device…',
'settings.audioIo.previousSelectedInput': 'Previously Selected Input (permission required to verify)',
'settings.audioIo.previousSelectedOutput': 'Previously Selected Output (permission required to verify)',
'settings.audioIo.status.inputUnavailable': 'Previously selected audio input device is unavailable; using System Default.',
'settings.audioIo.status.outputUnavailable': 'Previously selected audio output device is unavailable; using System Default.',
'settings.audioIo.status.refreshed': 'Audio device list refreshed.',
'settings.audioIo.status.readFailed': 'Unable to read audio devices from the browser.',
'settings.audioIo.status.inputDefaultNextSession': 'Audio input will use System Default on the next recording session.',
'settings.audioIo.status.inputChangedNextSession': 'Audio input will change on the next recording session.',
'settings.audioIo.status.outputDefaultAfterRefresh': 'Audio output will use System Default after refresh.',
'settings.audioIo.status.outputSavedRefreshRequired': 'Audio output device saved. Refresh the page to apply it in v1.',
'settings.audioIo.status.outputPromptUnsupported': 'This browser does not support prompting for audio output devices.',
'settings.audioIo.status.outputSelectedRefreshRequired': 'Output device selected. Refresh the page to apply it in v1.',
'settings.audioIo.status.outputSelectionDenied': 'The browser did not allow selecting a non-default output device.',
'settings.behavior.title': 'Behavior',
'settings.behavior.editor': 'Editor',
'settings.behavior.playheadUpdateFrequency': 'Playhead Update Frequency (fps)',
'settings.behavior.playheadUpdateFrequencyHelp': 'Update frequency for the playhead animation during playback. Higher values (60 fps) provide smoother animation but use more CPU. Lower values (10 fps) are more efficient. Changes apply immediately without restart.',
'settings.behavior.spectrogramHeightResolution': 'Spectrogram Height Resolution',
'settings.behavior.spectrogramHeightResolutionHelp': 'Controls vertical spectrogram detail across the visible pitch range. Higher values sharpen pitch contours but use more CPU and memory while computing the spectrogram.',
'settings.behavior.chatBox': 'Chat Box',
'settings.behavior.openAtStartup': 'Open at Start Up',
'settings.behavior.audio': 'Audio',
'settings.behavior.lookaheadTime': 'Lookahead Time (ms)',
'settings.behavior.lookaheadTimeHelp': 'Audio scheduling lookahead time (0-500ms). Lower values (10-20ms) reduce MIDI input latency but may cause audio glitches on slower systems. Higher values (100ms+) are better for playback stability. Changes apply immediately without restart.',
'settings.behavior.playbackDelay': 'Playback Delay (ms)',
'settings.behavior.playbackDelayHelp': 'Playback will start with a short delay after pressing the start button (0-500ms). Increasing this value might help stabilize playback, especially for the first few ticks if the lookahead value is too low. Changes apply immediately without restart.',
'settings.behavior.midiAutomationInterpolation': 'MIDI Automation Interpolation',
'settings.behavior.midiAutomationInterpolation.low': 'Low-end (20 ms)',
'settings.behavior.midiAutomationInterpolation.balanced': 'Balanced (10 ms)',
'settings.behavior.midiAutomationInterpolation.high': 'High quality (5 ms)',
'settings.behavior.midiAutomationInterpolationHelp': 'Controls how densely MIDI automation are baked for playback and bounce. Smaller intervals sound smoother but schedule more events.',
'settings.behavior.midiInputLatency': 'MIDI Input Latency (ms)',
'settings.behavior.midiInputLatencyHelp': 'Timing correction for MIDI recording (0-500ms). If recorded notes appear slightly late compared to where you intended to play them, increase this value to match your MIDI device\'s input latency. Each note\'s position is shifted back by this amount when committed.',
'settings.behavior.bounceStartsFromBeat1': 'Bounce Starts From Beat 1',
'settings.behavior.bounceStartsFromBeat1Help': 'Yes includes leading silence from the start of the song up to the first rendered region when bouncing WAV/MP3. No trims that leading silence and starts bounce at the first rendered note or audio region.',
'settings.behavior.captureAudioForScreenSharing': 'Capture Audio for Screen Sharing',
'settings.behavior.captureAudioForScreenSharingHelp': 'Restart KGStudio (refresh the page) to take effect. Enable this option when KGStudio\'s audio cannot be captured during screen sharing in video calls (e.g., Zoom, Teams). This creates an additional audio stream that screen capture applications can detect.',
'settings.behavior.captureAudioForScreenSharingWarning': 'It is important to make sure when screen sharing in Zoom, the "Share Sound" option is enabled.',
'settings.behavior.validation.lookahead.invalidNumber': 'Lookahead time must be a valid number',
'settings.behavior.validation.lookahead.range': 'Lookahead time must be between 0 and 0.5 seconds (0-500ms)',
'settings.behavior.validation.playbackDelay.invalidNumber': 'Playback delay must be a valid number',
'settings.behavior.validation.playbackDelay.range': 'Playback delay must be between 0 and 0.5 seconds (0-500ms)',
'settings.behavior.validation.recordingOffset.invalidNumber': 'MIDI input latency must be a valid number',
'settings.behavior.validation.recordingOffset.range': 'MIDI input latency must be between 0 and 0.5 seconds (0-500ms)',
'settings.templates.title': 'Templates',
'settings.templates.customInstructions': 'Custom Instructions',
'settings.templates.placeholder': 'Please input your custom instructions for the K.G.Studio Musician Assistant',
'settings.chordGuide.title': 'Chord Guide',
'settings.chordGuide.resetToDefault': 'Reset to Default',
'settings.chordGuide.group.major': 'Major Candidate Chords',
'settings.chordGuide.group.minor': 'Minor Candidate Chords',
'settings.chordGuide.help.major': 'Enter chords relative to C major. The app will transpose them automatically for other major key signatures.',
'settings.chordGuide.help.minor': 'Enter chords relative to A minor. The app will transpose them automatically for other minor key signatures.',
'settings.chordGuide.functionsAria': '{group} functions',
'settings.chordGuide.addTitle': 'Add {group} chord',
'settings.chordGuide.deleteSelectedRows': 'Delete selected rows',
'settings.chordGuide.table.chord': 'Chord',
'settings.chordGuide.table.notes': 'Notes',
'settings.chordGuide.table.source': 'Source',
'settings.chordGuide.table.note': 'Note',
'settings.chordGuide.loadError': 'Failed to load chord guide settings.',
'settings.chordGuide.invalidChordSymbol': 'Please enter a valid chord symbol. Example: Bm7b5',
'settings.chordGuide.unsupportedChordSymbol': 'Unable to derive notes for this chord. Please use a supported chord symbol.',
'dialog.title.notice': 'Notice',
'dialog.title.timeSignature': 'Time Signature',
'dialog.title.tempoDetection': 'Tempo Detection',
+74
View File
@@ -70,6 +70,80 @@ export const frFrMessages: TranslationMessages = {
'settings.general.kgone.enabledOption': 'Activé',
'settings.general.kgone.serverBaseUrl': 'URL de base du serveur',
'settings.general.kgone.serverBaseUrlHelp': 'URL de base dun serveur K.G.One Music Studio en cours dexécution. Utilisée pour la génération de morceau complet, de clip et la séparation de stems.',
'settings.audioIo.title': 'Entrées/Sorties audio',
'settings.audioIo.deviceRouting': 'Routage des appareils',
'settings.audioIo.refresh': 'Rafraîchir la liste des appareils',
'settings.audioIo.refreshing': 'Rafraîchissement…',
'settings.audioIo.description': 'Choisissez les appareils que KGStudio doit utiliser pour lenregistrement et la lecture. Les changements dentrée sappliquent à la prochaine session denregistrement. Les changements de sortie demandent un rechargement de la page en v1.',
'settings.audioIo.inputDevice': 'Appareil dentrée audio',
'settings.audioIo.outputDevice': 'Appareil de sortie audio',
'settings.audioIo.inputHelp': 'Les changements sappliquent au prochain enregistrement audio. Si lappareil choisi est retiré, KGStudio reviendra à lappareil système par défaut.',
'settings.audioIo.outputHelp': 'Les changements de sortie demandent un rechargement en v1. La prise en charge du routage vers une sortie non par défaut est limitée dans les navigateurs, donc KGStudio continuera à utiliser la sortie système par défaut lorsquelle nest pas prise en charge.',
'settings.audioIo.outputSinkUnsupported': 'Ce navigateur nexpose pas de bascule fiable en direct de la sortie Web Audio. La sélection dune sortie non par défaut reste un effort au mieux et peut finalement rester sur la sortie système par défaut.',
'settings.audioIo.chooseOutputDevice': 'Choisir lappareil de sortie…',
'settings.audioIo.previousSelectedInput': 'Entrée précédemment sélectionnée (autorisation requise pour vérification)',
'settings.audioIo.previousSelectedOutput': 'Sortie précédemment sélectionnée (autorisation requise pour vérification)',
'settings.audioIo.status.inputUnavailable': 'Lappareil dentrée audio précédemment sélectionné nest pas disponible ; utilisation de lappareil système par défaut.',
'settings.audioIo.status.outputUnavailable': 'Lappareil de sortie audio précédemment sélectionné nest pas disponible ; utilisation de lappareil système par défaut.',
'settings.audioIo.status.refreshed': 'La liste des appareils audio a été rafraîchie.',
'settings.audioIo.status.readFailed': 'Impossible de lire les appareils audio depuis le navigateur.',
'settings.audioIo.status.inputDefaultNextSession': 'Lentrée audio utilisera lappareil système par défaut lors de la prochaine session denregistrement.',
'settings.audioIo.status.inputChangedNextSession': 'Lentrée audio changera lors de la prochaine session denregistrement.',
'settings.audioIo.status.outputDefaultAfterRefresh': 'La sortie audio utilisera lappareil système par défaut après rechargement.',
'settings.audioIo.status.outputSavedRefreshRequired': 'Lappareil de sortie audio a été enregistré. Rechargez la page pour lappliquer en v1.',
'settings.audioIo.status.outputPromptUnsupported': 'Ce navigateur ne prend pas en charge linvite de sélection dun appareil de sortie audio.',
'settings.audioIo.status.outputSelectedRefreshRequired': 'Appareil de sortie sélectionné. Rechargez la page pour lappliquer en v1.',
'settings.audioIo.status.outputSelectionDenied': 'Le navigateur na pas autorisé la sélection dune sortie non par défaut.',
'settings.behavior.title': 'Comportement',
'settings.behavior.editor': 'Éditeur',
'settings.behavior.playheadUpdateFrequency': 'Fréquence de mise à jour de la tête de lecture (fps)',
'settings.behavior.playheadUpdateFrequencyHelp': 'Fréquence de mise à jour de lanimation de la tête de lecture pendant la lecture. Des valeurs plus élevées (60 fps) donnent une animation plus fluide mais utilisent plus de CPU. Des valeurs plus faibles (10 fps) sont plus efficaces. Les changements sappliquent immédiatement sans redémarrage.',
'settings.behavior.spectrogramHeightResolution': 'Résolution verticale du spectrogramme',
'settings.behavior.spectrogramHeightResolutionHelp': 'Contrôle le niveau de détail vertical du spectrogramme sur la plage de hauteurs visible. Des valeurs plus élevées accentuent les contours de hauteur mais consomment davantage de CPU et de mémoire lors du calcul du spectrogramme.',
'settings.behavior.chatBox': 'Boîte de discussion',
'settings.behavior.openAtStartup': 'Ouvrir au démarrage',
'settings.behavior.audio': 'Audio',
'settings.behavior.lookaheadTime': 'Temps danticipation (ms)',
'settings.behavior.lookaheadTimeHelp': 'Temps danticipation de planification audio (0-500 ms). Des valeurs plus faibles (10-20 ms) réduisent la latence dentrée MIDI mais peuvent provoquer des glitches audio sur les systèmes plus lents. Des valeurs plus élevées (100 ms et plus) améliorent la stabilité de lecture. Les changements sappliquent immédiatement sans redémarrage.',
'settings.behavior.playbackDelay': 'Délai de lecture (ms)',
'settings.behavior.playbackDelayHelp': 'La lecture démarrera avec un léger délai après avoir appuyé sur le bouton de démarrage (0-500 ms). Augmenter cette valeur peut aider à stabiliser la lecture, surtout pendant les premiers ticks si la valeur danticipation est trop faible. Les changements sappliquent immédiatement sans redémarrage.',
'settings.behavior.midiAutomationInterpolation': 'Interpolation de lautomation MIDI',
'settings.behavior.midiAutomationInterpolation.low': 'Économie (20 ms)',
'settings.behavior.midiAutomationInterpolation.balanced': 'Équilibré (10 ms)',
'settings.behavior.midiAutomationInterpolation.high': 'Haute qualité (5 ms)',
'settings.behavior.midiAutomationInterpolationHelp': 'Contrôle la densité de cuisson de lautomation MIDI pour la lecture et le bounce. Des intervalles plus courts sonnent plus fluides mais planifient davantage d’événements.',
'settings.behavior.midiInputLatency': 'Latence dentrée MIDI (ms)',
'settings.behavior.midiInputLatencyHelp': 'Correction temporelle pour lenregistrement MIDI (0-500 ms). Si les notes enregistrées apparaissent légèrement en retard par rapport à ce que vous vouliez jouer, augmentez cette valeur pour laligner sur la latence dentrée de votre appareil MIDI. La position de chaque note est reculée de cette valeur lors de la validation.',
'settings.behavior.bounceStartsFromBeat1': 'Le bounce démarre au temps 1',
'settings.behavior.bounceStartsFromBeat1Help': 'Oui inclut le silence initial entre le début du morceau et la première région rendue lors du bounce WAV/MP3. Non supprime ce silence initial et démarre le bounce à la première note rendue ou à la première région audio.',
'settings.behavior.captureAudioForScreenSharing': 'Capturer laudio pour le partage d’écran',
'settings.behavior.captureAudioForScreenSharingHelp': 'Redémarrez KGStudio (rechargez la page) pour appliquer ce réglage. Activez cette option lorsque laudio de KGStudio ne peut pas être capturé pendant le partage d’écran dans les appels vidéo (ex. Zoom, Teams). Cela crée un flux audio supplémentaire que les applications de capture d’écran peuvent détecter.',
'settings.behavior.captureAudioForScreenSharingWarning': 'Lorsque vous partagez l’écran dans Zoom, vérifiez bien que loption "Share Sound" est activée.',
'settings.behavior.validation.lookahead.invalidNumber': 'Le temps danticipation doit être un nombre valide',
'settings.behavior.validation.lookahead.range': 'Le temps danticipation doit être compris entre 0 et 0,5 seconde (0-500 ms)',
'settings.behavior.validation.playbackDelay.invalidNumber': 'Le délai de lecture doit être un nombre valide',
'settings.behavior.validation.playbackDelay.range': 'Le délai de lecture doit être compris entre 0 et 0,5 seconde (0-500 ms)',
'settings.behavior.validation.recordingOffset.invalidNumber': 'La latence dentrée MIDI doit être un nombre valide',
'settings.behavior.validation.recordingOffset.range': 'La latence dentrée MIDI doit être comprise entre 0 et 0,5 seconde (0-500 ms)',
'settings.templates.title': 'Modèles',
'settings.templates.customInstructions': 'Instructions personnalisées',
'settings.templates.placeholder': 'Veuillez saisir vos instructions personnalisées pour lassistant musical K.G.Studio',
'settings.chordGuide.title': 'Guide daccords',
'settings.chordGuide.resetToDefault': 'Rétablir les valeurs par défaut',
'settings.chordGuide.group.major': 'Accords candidats majeurs',
'settings.chordGuide.group.minor': 'Accords candidats mineurs',
'settings.chordGuide.help.major': 'Saisissez des accords relatifs à Do majeur. Lapplication les transposera automatiquement pour les autres armures majeures.',
'settings.chordGuide.help.minor': 'Saisissez des accords relatifs à La mineur. Lapplication les transposera automatiquement pour les autres armures mineures.',
'settings.chordGuide.functionsAria': 'Fonctions pour {group}',
'settings.chordGuide.addTitle': 'Ajouter un accord à {group}',
'settings.chordGuide.deleteSelectedRows': 'Supprimer les lignes sélectionnées',
'settings.chordGuide.table.chord': 'Accord',
'settings.chordGuide.table.notes': 'Notes',
'settings.chordGuide.table.source': 'Source',
'settings.chordGuide.table.note': 'Note',
'settings.chordGuide.loadError': 'Impossible de charger les réglages du guide daccords.',
'settings.chordGuide.invalidChordSymbol': 'Veuillez saisir un symbole daccord valide. Exemple : Bm7b5',
'settings.chordGuide.unsupportedChordSymbol': 'Impossible de déduire les notes pour cet accord. Veuillez utiliser un symbole daccord pris en charge.',
'dialog.title.notice': 'Information',
'dialog.title.timeSignature': 'Chiffrage de mesure',
'dialog.title.tempoDetection': 'Détection du tempo',
+74
View File
@@ -72,6 +72,80 @@ export const zhCnMessages: TranslationMessages = {
'settings.general.kgone.enabledOption': '启用',
'settings.general.kgone.serverBaseUrl': '服务器基础 URL',
'settings.general.kgone.serverBaseUrlHelp': '正在运行的 K.G.One Music Studio 服务器基础 URL。用于整曲生成、片段生成和分轨。',
'settings.audioIo.title': '音频 I/O',
'settings.audioIo.deviceRouting': '设备路由',
'settings.audioIo.refresh': '刷新设备列表',
'settings.audioIo.refreshing': '刷新中…',
'settings.audioIo.description': '选择 KGStudio 用于录音和播放的设备。输入设备的更改会在下一次录音时生效。输出设备的更改在 v1 中需要刷新页面后生效。',
'settings.audioIo.inputDevice': '音频输入设备',
'settings.audioIo.outputDevice': '音频输出设备',
'settings.audioIo.inputHelp': '更改会在下一次录音时生效。如果所选设备被移除,KGStudio 会回退到系统默认设备。',
'settings.audioIo.outputHelp': '在 v1 中,输出设备更改需要刷新页面。浏览器对非默认输出路由的支持有限,因此在不支持时 KGStudio 会继续使用系统默认设备。',
'settings.audioIo.outputSinkUnsupported': '此浏览器未提供可靠的实时 Web Audio 输出切换能力。非默认输出设备选择仅尽力而为,最终可能仍会使用系统默认设备。',
'settings.audioIo.chooseOutputDevice': '选择输出设备…',
'settings.audioIo.previousSelectedInput': '先前选择的输入设备(需要权限才能验证)',
'settings.audioIo.previousSelectedOutput': '先前选择的输出设备(需要权限才能验证)',
'settings.audioIo.status.inputUnavailable': '先前选择的音频输入设备不可用;将使用系统默认设备。',
'settings.audioIo.status.outputUnavailable': '先前选择的音频输出设备不可用;将使用系统默认设备。',
'settings.audioIo.status.refreshed': '音频设备列表已刷新。',
'settings.audioIo.status.readFailed': '无法从浏览器读取音频设备。',
'settings.audioIo.status.inputDefaultNextSession': '音频输入将在下一次录音时使用系统默认设备。',
'settings.audioIo.status.inputChangedNextSession': '音频输入将在下一次录音时切换。',
'settings.audioIo.status.outputDefaultAfterRefresh': '刷新后音频输出将使用系统默认设备。',
'settings.audioIo.status.outputSavedRefreshRequired': '音频输出设备已保存。在 v1 中请刷新页面以应用。',
'settings.audioIo.status.outputPromptUnsupported': '此浏览器不支持弹出音频输出设备选择。',
'settings.audioIo.status.outputSelectedRefreshRequired': '已选择输出设备。请刷新页面以在 v1 中应用。',
'settings.audioIo.status.outputSelectionDenied': '浏览器不允许选择非默认输出设备。',
'settings.behavior.title': '行为',
'settings.behavior.editor': '编辑器',
'settings.behavior.playheadUpdateFrequency': '播放头更新频率(fps',
'settings.behavior.playheadUpdateFrequencyHelp': '播放时播放头动画的更新频率。更高的值(60 fps)会带来更平滑的动画,但会占用更多 CPU。更低的值(10 fps)更节省资源。更改会立即生效,无需重启。',
'settings.behavior.spectrogramHeightResolution': '频谱图高度分辨率',
'settings.behavior.spectrogramHeightResolutionHelp': '控制可见音高范围内的频谱图垂直细节。更高的值会让音高轮廓更清晰,但在计算频谱图时会消耗更多 CPU 和内存。',
'settings.behavior.chatBox': '聊天框',
'settings.behavior.openAtStartup': '启动时打开',
'settings.behavior.audio': '音频',
'settings.behavior.lookaheadTime': '前瞻时间(ms',
'settings.behavior.lookaheadTimeHelp': '音频调度前瞻时间(0-500ms)。较低的值(10-20ms)可以降低 MIDI 输入延迟,但在较慢的系统上可能造成音频卡顿。较高的值(100ms 以上)更有利于播放稳定性。更改会立即生效,无需重启。',
'settings.behavior.playbackDelay': '播放延迟(ms',
'settings.behavior.playbackDelayHelp': '按下开始按钮后,播放会有一个很短的延迟(0-500ms)。增加此值可能有助于稳定播放,特别是在前瞻值较低时的前几个节拍。更改会立即生效,无需重启。',
'settings.behavior.midiAutomationInterpolation': 'MIDI 自动化插值',
'settings.behavior.midiAutomationInterpolation.low': '低配(20 ms',
'settings.behavior.midiAutomationInterpolation.balanced': '平衡(10 ms',
'settings.behavior.midiAutomationInterpolation.high': '高质量(5 ms',
'settings.behavior.midiAutomationInterpolationHelp': '控制在播放和导出时将 MIDI 自动化烘焙得多密集。间隔越小声音越平滑,但会调度更多事件。',
'settings.behavior.midiInputLatency': 'MIDI 输入延迟(ms',
'settings.behavior.midiInputLatencyHelp': '用于 MIDI 录音的时间校正(0-500ms)。如果录下来的音符相比你实际想要演奏的位置略晚,可以增加此值以匹配你的 MIDI 设备输入延迟。每个音符在提交时都会向前回移这段时间。',
'settings.behavior.bounceStartsFromBeat1': '导出从第 1 拍开始',
'settings.behavior.bounceStartsFromBeat1Help': '选择“是”时,导出 WAV/MP3 会包含从歌曲开头到第一个被渲染区域之前的前导静音。选择“否”时,会裁掉这段前导静音,并从第一个被渲染的音符或音频区域开始导出。',
'settings.behavior.captureAudioForScreenSharing': '为屏幕共享捕获音频',
'settings.behavior.captureAudioForScreenSharingHelp': '需要重启 KGStudio(刷新页面)后生效。当在视频会议中共享屏幕时,如果 KGStudio 的音频无法被捕获,请启用此选项(例如 Zoom、Teams)。这会创建一个额外的音频流,供屏幕捕获应用检测。',
'settings.behavior.captureAudioForScreenSharingWarning': '在 Zoom 中共享屏幕时,请务必确认已启用“共享声音”选项。',
'settings.behavior.validation.lookahead.invalidNumber': '前瞻时间必须是有效数字',
'settings.behavior.validation.lookahead.range': '前瞻时间必须在 0 到 0.5 秒之间(0-500ms',
'settings.behavior.validation.playbackDelay.invalidNumber': '播放延迟必须是有效数字',
'settings.behavior.validation.playbackDelay.range': '播放延迟必须在 0 到 0.5 秒之间(0-500ms',
'settings.behavior.validation.recordingOffset.invalidNumber': 'MIDI 输入延迟必须是有效数字',
'settings.behavior.validation.recordingOffset.range': 'MIDI 输入延迟必须在 0 到 0.5 秒之间(0-500ms',
'settings.templates.title': '模板',
'settings.templates.customInstructions': '自定义指令',
'settings.templates.placeholder': '请输入你为 K.G.Studio 音乐创作助手准备的自定义指令',
'settings.chordGuide.title': '和弦指导',
'settings.chordGuide.resetToDefault': '恢复默认',
'settings.chordGuide.group.major': '大调候选和弦',
'settings.chordGuide.group.minor': '小调候选和弦',
'settings.chordGuide.help.major': '请输入相对于 C 大调的和弦。应用会自动将其转调到其他大调调号。',
'settings.chordGuide.help.minor': '请输入相对于 A 小调的和弦。应用会自动将其转调到其他小调调号。',
'settings.chordGuide.functionsAria': '{group} 功能',
'settings.chordGuide.addTitle': '添加 {group}',
'settings.chordGuide.deleteSelectedRows': '删除所选行',
'settings.chordGuide.table.chord': '和弦',
'settings.chordGuide.table.notes': '音符',
'settings.chordGuide.table.source': '来源',
'settings.chordGuide.table.note': '备注',
'settings.chordGuide.loadError': '加载和弦指导设置失败。',
'settings.chordGuide.invalidChordSymbol': '请输入有效的和弦符号。示例:Bm7b5',
'settings.chordGuide.unsupportedChordSymbol': '无法为该和弦推导音符。请使用受支持的和弦符号。',
'dialog.title.notice': '提示',
'dialog.title.timeSignature': '拍号',
'dialog.title.tempoDetection': '速度检测',
+74
View File
@@ -72,6 +72,80 @@ export const zhHkMessages: TranslationMessages = {
'settings.general.kgone.enabledOption': '啟用',
'settings.general.kgone.serverBaseUrl': '服務器基礎 URL',
'settings.general.kgone.serverBaseUrlHelp': '正在運行的 K.G.One Music Studio 服務器基礎 URL。用於整曲生成、片段生成和分軌。',
'settings.audioIo.title': '音頻 I/O',
'settings.audioIo.deviceRouting': '裝置路由',
'settings.audioIo.refresh': '重新整理裝置清單',
'settings.audioIo.refreshing': '重新整理中…',
'settings.audioIo.description': '選擇 KGStudio 用於錄音與播放的裝置。輸入裝置的變更會在下一次錄音時生效。輸出裝置的變更在 v1 中需要重新整理頁面後生效。',
'settings.audioIo.inputDevice': '音頻輸入裝置',
'settings.audioIo.outputDevice': '音頻輸出裝置',
'settings.audioIo.inputHelp': '變更會在下一次錄音時生效。如果所選裝置被移除,KGStudio 會回退到系統預設裝置。',
'settings.audioIo.outputHelp': '在 v1 中,輸出裝置變更需要重新整理頁面。瀏覽器對非預設輸出路由的支援有限,因此在不支援時 KGStudio 會繼續使用系統預設裝置。',
'settings.audioIo.outputSinkUnsupported': '此瀏覽器未提供可靠的即時 Web Audio 輸出切換能力。非預設輸出裝置選擇僅屬盡力而為,最終可能仍會使用系統預設裝置。',
'settings.audioIo.chooseOutputDevice': '選擇輸出裝置…',
'settings.audioIo.previousSelectedInput': '先前選擇的輸入裝置(需要權限才能驗證)',
'settings.audioIo.previousSelectedOutput': '先前選擇的輸出裝置(需要權限才能驗證)',
'settings.audioIo.status.inputUnavailable': '先前選擇的音頻輸入裝置不可用;將使用系統預設裝置。',
'settings.audioIo.status.outputUnavailable': '先前選擇的音頻輸出裝置不可用;將使用系統預設裝置。',
'settings.audioIo.status.refreshed': '音頻裝置清單已重新整理。',
'settings.audioIo.status.readFailed': '無法從瀏覽器讀取音頻裝置。',
'settings.audioIo.status.inputDefaultNextSession': '音頻輸入將在下一次錄音時使用系統預設裝置。',
'settings.audioIo.status.inputChangedNextSession': '音頻輸入將在下一次錄音時切換。',
'settings.audioIo.status.outputDefaultAfterRefresh': '重新整理後音頻輸出將使用系統預設裝置。',
'settings.audioIo.status.outputSavedRefreshRequired': '音頻輸出裝置已儲存。在 v1 中請重新整理頁面以套用。',
'settings.audioIo.status.outputPromptUnsupported': '此瀏覽器不支援彈出音頻輸出裝置選擇。',
'settings.audioIo.status.outputSelectedRefreshRequired': '已選擇輸出裝置。請重新整理頁面以在 v1 中套用。',
'settings.audioIo.status.outputSelectionDenied': '瀏覽器不允許選擇非預設輸出裝置。',
'settings.behavior.title': '行為',
'settings.behavior.editor': '編輯器',
'settings.behavior.playheadUpdateFrequency': '播放頭更新頻率(fps',
'settings.behavior.playheadUpdateFrequencyHelp': '播放時播放頭動畫的更新頻率。較高的值(60 fps)會帶來更平滑的動畫,但會佔用更多 CPU。較低的值(10 fps)更節省資源。變更會立即生效,無需重新啟動。',
'settings.behavior.spectrogramHeightResolution': '頻譜圖高度解析度',
'settings.behavior.spectrogramHeightResolutionHelp': '控制可見音高範圍內的頻譜圖垂直細節。較高的值會讓音高輪廓更清晰,但在計算頻譜圖時會消耗更多 CPU 與記憶體。',
'settings.behavior.chatBox': '聊天框',
'settings.behavior.openAtStartup': '啟動時開啟',
'settings.behavior.audio': '音頻',
'settings.behavior.lookaheadTime': '前瞻時間(ms',
'settings.behavior.lookaheadTimeHelp': '音頻排程前瞻時間(0-500ms)。較低的值(10-20ms)可降低 MIDI 輸入延遲,但在較慢的系統上可能造成音頻卡頓。較高的值(100ms 以上)更有利於播放穩定性。變更會立即生效,無需重新啟動。',
'settings.behavior.playbackDelay': '播放延遲(ms',
'settings.behavior.playbackDelayHelp': '按下開始按鈕後,播放會有一個很短的延遲(0-500ms)。增加此值可能有助於穩定播放,特別是在前瞻值較低時的前幾個節拍。變更會立即生效,無需重新啟動。',
'settings.behavior.midiAutomationInterpolation': 'MIDI 自動化插值',
'settings.behavior.midiAutomationInterpolation.low': '低配(20 ms',
'settings.behavior.midiAutomationInterpolation.balanced': '平衡(10 ms',
'settings.behavior.midiAutomationInterpolation.high': '高品質(5 ms',
'settings.behavior.midiAutomationInterpolationHelp': '控制在播放與匯出時將 MIDI 自動化烘焙得多密集。間隔越小聲音越平滑,但會排程更多事件。',
'settings.behavior.midiInputLatency': 'MIDI 輸入延遲(ms',
'settings.behavior.midiInputLatencyHelp': '用於 MIDI 錄音的時間校正(0-500ms)。如果錄下來的音符比你實際想演奏的位置略晚,可以增加此值以匹配你的 MIDI 裝置輸入延遲。每個音符在提交時都會向前回移這段時間。',
'settings.behavior.bounceStartsFromBeat1': '匯出從第 1 拍開始',
'settings.behavior.bounceStartsFromBeat1Help': '選擇「是」時,匯出 WAV/MP3 會包含從歌曲開頭到第一個被渲染區域之前的前導靜音。選擇「否」時,會裁掉這段前導靜音,並從第一個被渲染的音符或音頻區域開始匯出。',
'settings.behavior.captureAudioForScreenSharing': '為螢幕分享擷取音頻',
'settings.behavior.captureAudioForScreenSharingHelp': '需要重新啟動 KGStudio(重新整理頁面)後生效。當在視訊會議中分享螢幕時,如果 KGStudio 的音頻無法被擷取,請啟用此選項(例如 Zoom、Teams)。這會建立一個額外的音頻串流,供螢幕擷取應用程式偵測。',
'settings.behavior.captureAudioForScreenSharingWarning': '在 Zoom 中分享螢幕時,請務必確認已啟用「Share Sound」選項。',
'settings.behavior.validation.lookahead.invalidNumber': '前瞻時間必須是有效數字',
'settings.behavior.validation.lookahead.range': '前瞻時間必須在 0 到 0.5 秒之間(0-500ms',
'settings.behavior.validation.playbackDelay.invalidNumber': '播放延遲必須是有效數字',
'settings.behavior.validation.playbackDelay.range': '播放延遲必須在 0 到 0.5 秒之間(0-500ms',
'settings.behavior.validation.recordingOffset.invalidNumber': 'MIDI 輸入延遲必須是有效數字',
'settings.behavior.validation.recordingOffset.range': 'MIDI 輸入延遲必須在 0 到 0.5 秒之間(0-500ms',
'settings.templates.title': '模板',
'settings.templates.customInstructions': '自訂指示',
'settings.templates.placeholder': '請輸入你為 K.G.Studio 音樂創作助手準備的自訂指示',
'settings.chordGuide.title': '和弦指導',
'settings.chordGuide.resetToDefault': '恢復預設',
'settings.chordGuide.group.major': '大調候選和弦',
'settings.chordGuide.group.minor': '小調候選和弦',
'settings.chordGuide.help.major': '請輸入相對於 C 大調的和弦。應用程式會自動將其移調到其他大調調號。',
'settings.chordGuide.help.minor': '請輸入相對於 A 小調的和弦。應用程式會自動將其移調到其他小調調號。',
'settings.chordGuide.functionsAria': '{group} 功能',
'settings.chordGuide.addTitle': '新增 {group}',
'settings.chordGuide.deleteSelectedRows': '刪除所選列',
'settings.chordGuide.table.chord': '和弦',
'settings.chordGuide.table.notes': '音符',
'settings.chordGuide.table.source': '來源',
'settings.chordGuide.table.note': '備註',
'settings.chordGuide.loadError': '載入和弦指導設定失敗。',
'settings.chordGuide.invalidChordSymbol': '請輸入有效的和弦符號。範例:Bm7b5',
'settings.chordGuide.unsupportedChordSymbol': '無法為這個和弦推導音符。請使用受支援的和弦符號。',
'dialog.title.notice': '提示',
'dialog.title.timeSignature': '拍號',
'dialog.title.tempoDetection': '速度檢測',