feat: added i18n support; added Simplified Chinese support

This commit is contained in:
Xiaohan-Tian
2026-05-30 20:02:55 -07:00
parent c17d169dc6
commit 957f6db950
58 changed files with 3354 additions and 520 deletions
+88
View File
@@ -0,0 +1,88 @@
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { act, render, screen, waitFor } from '@testing-library/react';
import { I18nProvider } from './I18nProvider';
import { useI18n } from './useI18n';
const configState = new Map<string, unknown>([['general.language', 'auto']]);
const listeners = new Set<(changedKeys: string[]) => void>();
const configManagerMock = {
getIsInitialized: vi.fn(() => true),
initialize: vi.fn().mockResolvedValue(undefined),
get: vi.fn((key: string) => configState.get(key)),
set: vi.fn(async (key: string, value: unknown) => {
configState.set(key, value);
for (const listener of listeners) {
listener([key]);
}
}),
addChangeListener: vi.fn((listener: (changedKeys: string[]) => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
}),
};
vi.mock('../core/config/ConfigManager', () => ({
ConfigManager: {
instance: () => configManagerMock,
},
}));
function TestComponent() {
const { t, resolvedLocale } = useI18n();
return (
<div>
<span data-testid="locale">{resolvedLocale}</span>
<span>{t('settings.sidebar.title')}</span>
</div>
);
}
describe('I18nProvider', () => {
it('resolves auto to English by default', async () => {
Object.defineProperty(window.navigator, 'languages', {
configurable: true,
value: ['en-US'],
});
render(
<I18nProvider>
<TestComponent />
</I18nProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('locale').textContent).toBe('en_us');
expect(screen.getByText('Settings')).toBeTruthy();
});
});
it('resolves auto to Chinese and updates instantly when config changes', async () => {
configState.set('general.language', 'auto');
Object.defineProperty(window.navigator, 'languages', {
configurable: true,
value: ['zh-CN'],
});
render(
<I18nProvider>
<TestComponent />
</I18nProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('locale').textContent).toBe('zh_cn');
expect(screen.getByText('设置')).toBeTruthy();
});
await act(async () => {
await configManagerMock.set('general.language', 'en_us');
});
await waitFor(() => {
expect(screen.getByTestId('locale').textContent).toBe('en_us');
expect(screen.getByText('Settings')).toBeTruthy();
});
});
});
+76
View File
@@ -0,0 +1,76 @@
import React, { createContext, useEffect, useMemo, useState } from 'react';
import { ConfigManager } from '../core/config/ConfigManager';
import { normalizeLanguageSetting, resolveLanguageSetting } from './locale';
import { setCurrentLocale, translate } from './translate';
import type { I18nContextValue, LanguageSetting, ResolvedLocaleCode, TranslationParams } from './types';
const defaultValue: I18nContextValue = {
languageSetting: 'auto',
resolvedLocale: 'en_us',
setLanguageSetting: async () => undefined,
t: (key: string, params?: TranslationParams) => translate(key, params, 'en_us'),
};
export const I18nContext = createContext<I18nContextValue>(defaultValue);
function buildState(configManager: ConfigManager): { languageSetting: LanguageSetting; resolvedLocale: ResolvedLocaleCode } {
const languageSetting = normalizeLanguageSetting(configManager.get('general.language'));
const resolvedLocale = resolveLanguageSetting(languageSetting);
return { languageSetting, resolvedLocale };
}
export const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [languageSetting, setLanguageSettingState] = useState<LanguageSetting>('auto');
const [resolvedLocale, setResolvedLocaleState] = useState<ResolvedLocaleCode>('en_us');
useEffect(() => {
let mounted = true;
let unsubscribe: (() => void) | undefined;
const configManager = ConfigManager.instance();
const syncFromConfig = () => {
const nextState = buildState(configManager);
setCurrentLocale(nextState.resolvedLocale);
if (!mounted) {
return;
}
setLanguageSettingState(nextState.languageSetting);
setResolvedLocaleState(nextState.resolvedLocale);
};
const initialize = async () => {
if (!configManager.getIsInitialized()) {
await configManager.initialize();
}
syncFromConfig();
unsubscribe = configManager.addChangeListener((changedKeys) => {
if (changedKeys.includes('__all__') || changedKeys.includes('general.language')) {
syncFromConfig();
}
});
};
void initialize();
return () => {
mounted = false;
unsubscribe?.();
};
}, []);
const contextValue = useMemo<I18nContextValue>(() => ({
languageSetting,
resolvedLocale,
setLanguageSetting: async (value: LanguageSetting) => {
await ConfigManager.instance().set('general.language', value);
},
t: (key: string, params?: TranslationParams) => translate(key, params, resolvedLocale),
}), [languageSetting, resolvedLocale]);
return (
<I18nContext.Provider value={contextValue}>
{children}
</I18nContext.Provider>
);
};
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { getEnglishInstrumentDisplayName, getInstrumentDisplayName, getInstrumentGroupLabel } from './instruments';
import { translate } from './translate';
describe('instrument i18n helpers', () => {
it('returns translated instrument display names for supported locales', () => {
expect(getInstrumentDisplayName('acoustic_grand_piano', (key, params) => translate(key, params, 'en_us'))).toBe('Acoustic Grand Piano');
expect(getInstrumentDisplayName('acoustic_grand_piano', (key, params) => translate(key, params, 'zh_cn'))).toBe('原声大钢琴');
});
it('returns translated instrument group labels for supported locales', () => {
expect(getInstrumentGroupLabel('PIANO_AND_KEYBOARDS', (key, params) => translate(key, params, 'en_us'))).toBe('Piano and Keyboards');
expect(getInstrumentGroupLabel('PIANO_AND_KEYBOARDS', (key, params) => translate(key, params, 'zh_cn'))).toBe('钢琴与键盘');
});
it('keeps stable English fallback names available for non-UI callers', () => {
expect(getEnglishInstrumentDisplayName('standard')).toBe('Standard Drum Kit');
expect(getEnglishInstrumentDisplayName('trumpet')).toBe('Trumpet');
});
});
+25
View File
@@ -0,0 +1,25 @@
import { FLUIDR3_INSTRUMENT_MAP, INSTRUMENT_GROUPS } from '../constants/generalMidiConstants';
import type { InstrumentType } from '../core/track/KGMidiTrack';
import type { TranslationParams } from './types';
type TranslateFn = (key: string, params?: TranslationParams) => string;
export type InstrumentGroupKey = keyof typeof INSTRUMENT_GROUPS;
export function getInstrumentDisplayName(instrumentKey: InstrumentType, t: TranslateFn): string {
const instrument = FLUIDR3_INSTRUMENT_MAP[instrumentKey];
if (!instrument) {
return String(instrumentKey);
}
return t(`instrument.name.${instrumentKey}`) || instrument.displayName;
}
export function getInstrumentGroupLabel(groupKey: InstrumentGroupKey, t: TranslateFn): string {
const englishLabel = INSTRUMENT_GROUPS[groupKey];
return t(`instrument.group.${groupKey}`) || englishLabel;
}
export function getEnglishInstrumentDisplayName(instrumentKey: InstrumentType): string {
return FLUIDR3_INSTRUMENT_MAP[instrumentKey]?.displayName || String(instrumentKey);
}
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { normalizeLanguageSetting, resolveLanguageSetting } from './locale';
describe('locale resolution', () => {
it('normalizes supported language settings', () => {
expect(normalizeLanguageSetting('auto')).toBe('auto');
expect(normalizeLanguageSetting('en_us')).toBe('en_us');
expect(normalizeLanguageSetting('zh_cn')).toBe('zh_cn');
expect(normalizeLanguageSetting('unknown')).toBe('auto');
});
it('resolves auto to English for non-Chinese locales', () => {
expect(resolveLanguageSetting('auto', ['en-US'])).toBe('en_us');
});
it('resolves auto to Simplified Chinese for Chinese locales', () => {
expect(resolveLanguageSetting('auto', ['zh-CN'])).toBe('zh_cn');
expect(resolveLanguageSetting('auto', ['zh-SG'])).toBe('zh_cn');
expect(resolveLanguageSetting('auto', ['zh-TW'])).toBe('zh_cn');
expect(resolveLanguageSetting('auto', ['zh-HK'])).toBe('zh_cn');
expect(resolveLanguageSetting('auto', ['zh-MO'])).toBe('zh_cn');
});
it('falls back to English when locale list is empty or unknown', () => {
expect(resolveLanguageSetting('auto', [])).toBe('en_us');
expect(resolveLanguageSetting('auto', ['fr-FR'])).toBe('en_us');
});
it('keeps explicit locale selections', () => {
expect(resolveLanguageSetting('en_us', ['zh-CN'])).toBe('en_us');
expect(resolveLanguageSetting('zh_cn', ['en-US'])).toBe('zh_cn');
});
});
+40
View File
@@ -0,0 +1,40 @@
import type { LanguageSetting, ResolvedLocaleCode } from './types';
const CHINESE_LANGUAGE_PREFIX = 'zh';
const DEFAULT_LOCALE: ResolvedLocaleCode = 'en_us';
export function normalizeLanguageSetting(value: unknown): LanguageSetting {
return value === 'zh_cn' || value === 'en_us' || value === 'auto' ? value : 'auto';
}
export function getBrowserLocales(): string[] {
if (typeof navigator === 'undefined') {
return [];
}
if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
return navigator.languages.filter((value): value is string => typeof value === 'string' && value.length > 0);
}
return typeof navigator.language === 'string' && navigator.language.length > 0
? [navigator.language]
: [];
}
export function resolveLanguageSetting(
setting: LanguageSetting,
browserLocales: string[] = getBrowserLocales(),
): ResolvedLocaleCode {
if (setting === 'en_us' || setting === 'zh_cn') {
return setting;
}
for (const locale of browserLocales) {
const normalizedLocale = locale.trim().toLowerCase();
if (normalizedLocale === CHINESE_LANGUAGE_PREFIX || normalizedLocale.startsWith(`${CHINESE_LANGUAGE_PREFIX}-`)) {
return 'zh_cn';
}
}
return DEFAULT_LOCALE;
}
+510
View File
@@ -0,0 +1,510 @@
import type { TranslationMessages } from '../types';
export const enUsMessages: TranslationMessages = {
'app.loading': 'Loading ...',
'status.chordGuideCandidate': 'Chord Guide Candidate: {name} - {notes} - {note}',
'settings.sidebar.title': 'Settings',
'settings.sidebar.close': 'Close Settings',
'settings.sidebar.general': 'General',
'settings.sidebar.audioIo': 'Audio I/O',
'settings.sidebar.behavior': 'Behavior',
'settings.sidebar.templates': 'Templates',
'settings.sidebar.chordGuide': 'Chord Guide',
'settings.general.title': 'General',
'settings.general.language.label': 'Language',
'settings.general.language.help': 'Auto uses your system language.',
'settings.general.language.auto': 'Auto',
'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': 'Simplified Chinese',
'settings.yes': 'Yes',
'settings.no': 'No',
'settings.restoreDefault': 'Restore default',
'settings.deleteCachedModel': 'Delete Cached Model',
'settings.deleting': 'Deleting...',
'settings.general.llmProvider.section': 'LLM Provider',
'settings.general.llmProvider.label': 'LLM Provider',
'settings.general.llmProvider.local': 'Local LLM (Browser)',
'settings.general.llmProvider.openai': 'OpenAI',
'settings.general.llmProvider.claudeOpenRouter': 'Claude (via OpenRouter)',
'settings.general.llmProvider.openaiCompatible': 'OpenAI Compatible (e.g. OpenRouter, Ollama)',
'settings.general.persistKeys.label': 'Persist API Keys on Non-Localhost',
'settings.general.persistKeys.help': 'When enabled, API keys will be saved to browser storage even on non-localhost environments. Warning: This may increase security vulnerability to XSS attacks.',
'settings.general.localRuntime.cachedStatus': 'Cached Model Status',
'settings.general.localRuntime.cacheChecking': 'Checking local model cache...',
'settings.general.localRuntime.cacheDownloaded': 'Downloaded in browser cache.',
'settings.general.localRuntime.cacheMissing': 'Not downloaded yet.',
'settings.general.localRuntime.contextLength': 'Context Length',
'settings.general.localRuntime.contextHelp': 'Larger context lengths require more VRAM and may also reduce performance as conversations become longer.',
'settings.general.localRuntime.downloadUrl': 'Download URL',
'settings.general.localRuntime.downloadHelp': 'Changing this URL may break downloads or point to an incompatible model file.',
'settings.general.localRuntime.autoDownload': 'The local model downloads automatically the next time you chat with `Local LLM (Browser)`.',
'settings.general.uvr5.section': 'UVR5 Web Runtime',
'settings.general.uvr5.downloadUrl': 'UVR-MDX-NET-Inst_HQ_3 Download URL',
'settings.general.htdemucs.downloadUrl': 'htdemucs_4s Download URL',
'settings.general.modelUrl.help': 'Changing this URL may break downloads or point to an incompatible model file.',
'settings.general.openai.section': 'OpenAI',
'settings.general.openai.key': 'Key',
'settings.general.openai.keyPlaceholder': 'Enter your OpenAI API key',
'settings.general.openai.model': 'Model',
'settings.general.openai.flexMode': 'Flex Mode',
'settings.general.openai.flexHelp': 'Flex Mode uses OpenAI\'s flexible service tier. Pros: potential cost savings and higher throughput during busy periods. Cons: variable latency and possible queueing/deprioritization. Applies only to the OpenAI provider; no effect for OpenAI Compatible servers.',
'settings.general.keys.persisted': 'Keys are persisted locally (the IndexedDB in your browser).',
'settings.general.keys.sessionOnly': 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.',
'settings.general.claudeOpenRouter.section': 'Anthropic Claude (via OpenRouter)',
'settings.general.claudeOpenRouter.keyPlaceholder': 'Enter your Claude API key',
'settings.general.baseUrl': 'Base URL',
'settings.general.claudeOpenRouter.baseUrlHelp': 'This is the base URL for the OpenRouter API. Please do not change this unless you know what you are doing.',
'settings.general.openaiCompatible.section': 'OpenAI Compatible Server',
'settings.general.openaiCompatible.keyPlaceholder': 'Enter your API key',
'settings.general.openaiCompatible.baseUrlPlaceholder': 'e.g. https://openrouter.ai/api/v1',
'settings.general.openaiCompatible.baseUrlHelp': 'Quick presets:',
'settings.general.openaiCompatible.modelPlaceholder': 'e.g. qwen3:30b',
'settings.general.soundfont.section': 'Soundfont Settings',
'settings.general.soundfont.managed': 'Soundfont configuration is managed by the server (kgone-server.json). Settings are read-only.',
'settings.general.soundfont.baseUrl': 'Base URL',
'settings.general.soundfont.baseUrlHelp': 'Changing this URL to an incompatible soundfont source may cause some instruments to sound wrong or not play.',
'settings.general.kgone.section': 'K.G.One Settings',
'settings.general.kgone.managed': 'K.G.One configuration is managed by the server (kgone-server.json). Settings are read-only.',
'settings.general.kgone.enabled': 'Enable K.G.One Integration',
'settings.general.kgone.disabled': 'Disabled',
'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.',
'dialog.title.notice': 'Notice',
'dialog.title.timeSignature': 'Time Signature',
'dialog.title.tempoDetection': 'Tempo Detection',
'dialog.title.applyTempo': 'Apply Tempo',
'dialog.title.chordDetection': 'Chord Detection',
'dialog.title.input': 'Input',
'dialog.title.confirm': 'Confirm',
'dialog.close': 'Close dialog',
'dialog.cancel': 'Cancel',
'dialog.ok': 'OK',
'dialog.experimentalFeature': 'Experimental Feature',
'dialog.recommendedSource': 'Recommended Source Material',
'dialog.chordHint.audio': 'Chord analysis is still experimental. Harmonic content, arrangement density, and transient-heavy material can affect accuracy. For more reliable results, start with the default settings, then refine sensitivity and stability until the detected harmony best matches the musical phrasing.',
'dialog.chordHint.midi': 'Chord analysis is still experimental. Voicing density, overlaps, and ornamental notes can influence the result. For more reliable chord labels, begin with the default settings, then adjust note suppression and harmonic focus to match the musical role of the passage.',
'dialog.chordHint.tempo': 'Tempo analysis is still experimental. Rubato phrasing, sparse transients, and layered percussion can reduce accuracy. Start with the default BPM range, then narrow the analysis window to the most plausible tempo span for the material if the first pass is not musically convincing.',
'dialog.sourceHint.kgone': 'For the most dependable chord labels, analyze a stem with vocals and percussion reduced or removed. If K.G.One Music Studio server integration is available, run Separator with the "Vocal, Drums, Bass, Guitar, Piano, and Others" model and use the Piano or Others stem for analysis.',
'dialog.sourceHint.local': 'For the most dependable chord labels, analyze a stem with vocals and percussion reduced or removed. If you are using the local separator, choose the "Vocal, Drums, Bass, and Others" model and use the Others stem for analysis.',
'dialog.label.sensitivity': 'Sensitivity',
'dialog.label.stability': 'Stability',
'dialog.label.noChordThreshold': 'No-Chord Threshold',
'dialog.label.enableSevenths': 'Chord Detail: Enable sevenths',
'dialog.label.shortNotes': 'Short Notes',
'dialog.option.suppressionLow': 'Low suppression',
'dialog.option.suppressionMedium': 'Medium suppression',
'dialog.option.suppressionHigh': 'High suppression',
'dialog.label.harmonicFocus': 'Harmonic Focus',
'dialog.option.harmonicBalanced': 'Balanced',
'dialog.option.harmonicSustained': 'Favor sustained notes',
'dialog.label.minimumBpm': 'Minimum BPM',
'dialog.label.maximumBpm': 'Maximum BPM',
'dialog.label.autoAlignRegionToBeat': 'Auto-align region to beat',
'eventList.title': 'Event List',
'eventList.scopeTabs': 'Event list scopes',
'eventList.scope.region': 'Region',
'eventList.scope.track': 'Track',
'eventList.scope.global': 'Global',
'eventList.deleteVisibleSelectedRows': 'Delete visible selected rows',
'eventList.table.position': 'Position',
'eventList.table.status': 'Status',
'eventList.table.num': 'Num',
'eventList.table.val': 'Val',
'eventList.table.lengthInfo': 'Length/Info',
'eventList.region.filters': 'Region event filters',
'eventList.region.filter.notes': 'Notes',
'eventList.region.filter.pitchBends': 'Pitch Bends',
'eventList.region.filter.controller': 'Controller',
'eventList.region.empty': 'Please select a MIDI region, or open one in the Piano Roll, to view its event list.',
'eventList.region.add.label': 'Add',
'eventList.region.add.noteTitle': 'Add note at playhead',
'eventList.region.add.pitchBendTitle': 'Add pitch bend at playhead',
'eventList.region.add.controllerTitle': 'Add controller event at playhead',
'eventList.region.addType.note': 'Note',
'eventList.region.addType.pitchBend': 'Pitch Bend',
'eventList.region.addType.controller': 'Controller',
'eventList.region.status.note': 'Note',
'eventList.region.status.pitchBend': 'Pitch Bend',
'eventList.region.status.controller': 'Controller',
'eventList.track.filters': 'Track list modes',
'eventList.track.filter.regions': 'Regions',
'eventList.track.filter.volume': 'Volume',
'eventList.track.filter.pan': 'Pan',
'eventList.track.empty': 'Please select a track to view regions and track automation.',
'eventList.track.add.label': 'Add',
'eventList.track.add.midiRegionTitle': 'Add MIDI region at playhead',
'eventList.track.add.volumeTitle': 'Add volume automation point at playhead',
'eventList.track.add.panTitle': 'Add pan automation point at playhead',
'eventList.track.addType.midiRegion': 'MIDI Region',
'eventList.track.status.audio': 'Audio',
'eventList.track.status.midi': 'MIDI',
'eventList.track.status.volume': 'Volume',
'eventList.track.status.pan': 'Pan',
'eventList.global.filters': 'Global event filters',
'eventList.global.filter.marker': 'Marker',
'eventList.global.filter.tempo': 'Tempo',
'eventList.global.filter.keySignature': 'Key Sig.',
'eventList.global.filter.chord': 'Chord',
'eventList.global.add.label': 'Add',
'eventList.global.add.markerTitle': 'Add marker region at playhead',
'eventList.global.add.tempoTitle': 'Add tempo region at playhead',
'eventList.global.add.keySignatureTitle': 'Add key signature region at playhead',
'eventList.global.add.chordTitle': 'Add chord region at playhead',
'eventList.global.addType.marker': 'Marker',
'eventList.global.addType.tempo': 'Tempo',
'eventList.global.addType.keySignature': 'Key Signature',
'eventList.global.addType.chord': 'Chord',
'eventList.global.status.marker': 'Marker',
'eventList.global.status.tempo': 'Tempo',
'eventList.global.status.keySignature': 'Key Signature',
'eventList.global.status.chord': 'Chord',
'eventList.global.validation.marker': 'Please enter a marker label. Expected a non-empty text label. Example: Intro',
'eventList.global.validation.tempo': 'Please enter a BPM value using digits only. Expected a whole number between {min} and {max}. Example: 128',
'eventList.global.validation.keySignature': 'Please enter one exact key signature name. Expected a canonical value such as "C major" or "F# minor". Example: F# minor',
'eventList.global.validation.chord': 'Please enter a valid chord symbol. Expected a chord representation the app can parse. Example: Bm7b5',
'eventList.global.validation.position': 'Please enter a position at or after the start of the project. Expected a non-negative location. Example: 1 1 0',
'eventList.global.validation.length': 'Please enter a positive length. Expected a duration greater than zero. Example: 4 0',
'globalTracks.label': 'Global tracks',
'globalTracks.marker': 'Marker',
'globalTracks.tempo': 'Tempo',
'globalTracks.signature': 'Key Signature',
'globalTracks.chord': 'Chord',
'globalTracks.addItem': 'Add {label} global track item',
'pianoRoll.power.linear': 'Linear',
'pianoRoll.power.sqrtDefault': '√ (default)',
'pianoRoll.power.mild': 'Mild',
'pianoRoll.power.strong': 'Strong',
'pianoRoll.chordGuide.off': 'Chord guide off',
'pianoRoll.chordGuide.tonic': 'Chord guide tonic',
'pianoRoll.chordGuide.subdominant': 'Chord guide subdominant',
'pianoRoll.chordGuide.dominant': 'Chord guide dominant',
'pianoRoll.spectrogramView': 'Spectrogram View',
'pianoRoll.sheetMusicView': 'Sheet Music View',
'pianoRoll.pointerTool': 'Pointer Tool',
'pianoRoll.pencilTool': 'Pencil Tool',
'pianoRoll.toggleAutomationLane': 'Toggle automation lane',
'pianoRoll.automation': 'Automation',
'pianoRoll.automationLane': '{label} automation lane',
'pianoRoll.automationType.pitchBend': 'Pitch Bend',
'pianoRoll.automationType.cc1': 'CC1',
'pianoRoll.automationType.cc2': 'CC2',
'pianoRoll.automationType.cc7': 'CC7',
'pianoRoll.automationType.cc11': 'CC11',
'pianoRoll.automationType.cc64': 'CC64',
'pianoRoll.mode': 'Mode',
'pianoRoll.modeOption.ionian': 'Ionian',
'pianoRoll.modeOption.dorian': 'Dorian',
'pianoRoll.modeOption.phrygian': 'Phrygian',
'pianoRoll.modeOption.lydian': 'Lydian',
'pianoRoll.modeOption.mixolydian': 'Mixolydian',
'pianoRoll.modeOption.aeolian': 'Aeolian',
'pianoRoll.modeOption.locrian': 'Locrian',
'pianoRoll.modeOption.harmonic_minor': 'Harmonic Minor',
'pianoRoll.modeOption.melodic_minor': 'Melodic Minor',
'pianoRoll.modeOption.phrygian_dominant': 'Phrygian Dominant',
'pianoRoll.modeOption.harmonic_major': 'Harmonic Major',
'pianoRoll.chordGuide': 'Chord guide',
'pianoRoll.sheetQuantization': 'Sheet Quant.',
'pianoRoll.snap': 'Snap',
'pianoRoll.snap.none': 'NO SNAP',
'pianoRoll.quantizePositionCompact': 'Qua. Pos.',
'pianoRoll.quantizeLengthCompact': 'Qua. Len.',
'pianoRoll.floor': 'Floor',
'pianoRoll.curve': 'Curve',
'pianoRoll.zoom': 'Zoom',
'pianoRoll.moreOptions': 'More options',
'pianoRoll.quantize.1/1': '1/1',
'pianoRoll.quantize.1/2': '1/2',
'pianoRoll.quantize.1/3': '1/3',
'pianoRoll.quantize.1/4': '1/4',
'pianoRoll.quantize.1/6': '1/6',
'pianoRoll.quantize.1/8': '1/8',
'pianoRoll.quantize.1/12': '1/12',
'pianoRoll.quantize.1/16': '1/16',
'pianoRoll.quantize.1/24': '1/24',
'pianoRoll.quantize.1/32': '1/32',
'pianoRoll.showActiveRegionOnly': 'Show Active Region Only',
'pianoRoll.showEntireTrack': 'Show Entire Track',
'pianoRoll.detectChords': 'Detect chords...',
'pianoRoll.detectingChords': 'Detecting chords...',
'pianoRoll.detectTempo': 'Detect tempo...',
'pianoRoll.detectingTempo': 'Detecting tempo...',
'percussion.short.35': 'Ac.Bass',
'percussion.short.36': 'BassDrum',
'percussion.short.37': 'SideStick',
'percussion.short.38': 'Ac.Snare',
'percussion.short.39': 'HandClap',
'percussion.short.40': 'ElecSnare',
'percussion.short.41': 'LowFloor',
'percussion.short.42': 'ClosedHH',
'percussion.short.43': 'HighFloor',
'percussion.short.44': 'PedalHH',
'percussion.short.45': 'LowTom',
'percussion.short.46': 'OpenHH',
'percussion.short.47': 'LowMidTom',
'percussion.short.48': 'HiMidTom',
'percussion.short.49': 'Crash1',
'percussion.short.50': 'HighTom',
'percussion.short.51': 'Ride1',
'percussion.short.52': 'Chinese',
'percussion.short.53': 'RideBell',
'percussion.short.54': 'Tambourine',
'percussion.short.55': 'Splash',
'percussion.short.56': 'Cowbell',
'percussion.short.57': 'Crash2',
'percussion.short.58': 'Vibraslap',
'percussion.short.59': 'Ride2',
'percussion.short.60': 'HiBongo',
'percussion.short.61': 'LowBongo',
'percussion.short.62': 'MuteHiCon',
'percussion.short.63': 'OpenHiCon',
'percussion.short.64': 'LowConga',
'percussion.short.65': 'HighTimba',
'percussion.short.66': 'LowTimba',
'percussion.short.67': 'HighAgo',
'percussion.short.68': 'LowAgo',
'percussion.short.69': 'Cabasa',
'percussion.short.70': 'Maracas',
'percussion.short.71': 'ShortWhis',
'percussion.short.72': 'LongWhis',
'percussion.short.73': 'ShortGui',
'percussion.short.74': 'LongGui',
'percussion.short.75': 'Claves',
'percussion.short.76': 'HiWood',
'percussion.short.77': 'LowWood',
'percussion.short.78': 'MuteCuica',
'percussion.short.79': 'OpenCuica',
'percussion.short.80': 'MuteTri',
'percussion.short.81': 'OpenTri',
'percussion.full.35': 'Acoustic Bass Drum',
'percussion.full.36': 'Bass Drum 1',
'percussion.full.37': 'Side Stick',
'percussion.full.38': 'Acoustic Snare',
'percussion.full.39': 'Hand Clap',
'percussion.full.40': 'Electric Snare',
'percussion.full.41': 'Low Floor Tom',
'percussion.full.42': 'Closed Hi Hat',
'percussion.full.43': 'High Floor Tom',
'percussion.full.44': 'Pedal Hi-Hat',
'percussion.full.45': 'Low Tom',
'percussion.full.46': 'Open Hi-Hat',
'percussion.full.47': 'Low-Mid Tom',
'percussion.full.48': 'Hi Mid Tom',
'percussion.full.49': 'Crash Cymbal 1',
'percussion.full.50': 'High Tom',
'percussion.full.51': 'Ride Cymbal 1',
'percussion.full.52': 'Chinese Cymbal',
'percussion.full.53': 'Ride Bell',
'percussion.full.54': 'Tambourine',
'percussion.full.55': 'Splash Cymbal',
'percussion.full.56': 'Cowbell',
'percussion.full.57': 'Crash Cymbal 2',
'percussion.full.58': 'Vibraslap',
'percussion.full.59': 'Ride Cymbal 2',
'percussion.full.60': 'Hi Bongo',
'percussion.full.61': 'Low Bongo',
'percussion.full.62': 'Mute Hi Conga',
'percussion.full.63': 'Open Hi Conga',
'percussion.full.64': 'Low Conga',
'percussion.full.65': 'High Timbale',
'percussion.full.66': 'Low Timbale',
'percussion.full.67': 'High Agogo',
'percussion.full.68': 'Low Agogo',
'percussion.full.69': 'Cabasa',
'percussion.full.70': 'Maracas',
'percussion.full.71': 'Short Whistle',
'percussion.full.72': 'Long Whistle',
'percussion.full.73': 'Short Guiro',
'percussion.full.74': 'Long Guiro',
'percussion.full.75': 'Claves',
'percussion.full.76': 'Hi Wood Block',
'percussion.full.77': 'Low Wood Block',
'percussion.full.78': 'Mute Cuica',
'percussion.full.79': 'Open Cuica',
'percussion.full.80': 'Mute Triangle',
'percussion.full.81': 'Open Triangle',
'toolbar.export.kgstudio': 'Export to KGStudio file',
'toolbar.export.midi': 'Export to MIDI file',
'toolbar.export.wav': 'Export to WAV',
'toolbar.export.mp3': 'Export to MP3',
'toolbar.projectName.prompt': 'Enter project name:',
'toolbar.projectName.invalid': 'Invalid project name. Only letters, numbers, spaces, hyphens, underscores, periods, and parentheses are allowed.',
'toolbar.projectName.reserved': '"{name}" is a reserved project name. Please choose a different name.',
'toolbar.projectName.overwrite': 'Project "{name}" already exists. Do you want to overwrite it?',
'toolbar.projectName.renameOrCopy': 'Would you like to rename this project, or save it as a new copy?',
'toolbar.projectName.saveAsCopy': 'Save as Copy',
'toolbar.projectName.rename': 'Rename',
'toolbar.save.error': 'An error occurred while saving: {error}',
'toolbar.load.error': 'An error occurred while loading the project: {error}',
'toolbar.newProject.confirm': 'Are you sure you want to create a new project? Any unsaved changes will be lost.',
'toolbar.project.notFound': 'Project "{name}" not found.',
'toolbar.openProject.confirm': 'Open this project? Any unsaved changes in the current project will be lost.',
'toolbar.status.loadFailed': 'Failed to load project: {error}',
'toolbar.status.newProjectCreated': 'New project "{name}" created',
'toolbar.status.projectLoaded': '{description} loaded successfully',
'toolbar.status.projectExportedKgstudio': 'Project "{name}" exported as KGStudio file',
'toolbar.status.exportProjectError': 'Error exporting project: {error}',
'toolbar.export.failedProject': 'Failed to export project: {error}',
'toolbar.status.projectExportedMidi': 'Project "{name}" exported as MIDI file',
'toolbar.status.exportMidiError': 'Error exporting MIDI: {error}',
'toolbar.export.failedMidi': 'Failed to export project as MIDI: {error}',
'toolbar.status.projectExportedWav': 'Project "{name}" exported as WAV file',
'toolbar.status.exportWavError': 'Error exporting WAV: {error}',
'toolbar.export.failedWav': 'Failed to export project as WAV: {error}',
'toolbar.status.projectExportedMp3': 'Project "{name}" exported as MP3 file',
'toolbar.status.exportMp3Error': 'Error exporting MP3: {error}',
'toolbar.export.failedMp3': 'Failed to export project as MP3: {error}',
'toolbar.status.importFailed': 'Failed to import file: {error}',
'toolbar.import.failedProjectFile': 'Failed to import project file: {error}',
'toolbar.import.corruptedKgstudio': 'The .kgstudio file is corrupted or invalid: {error}',
'toolbar.status.importingMidi': 'Importing MIDI file "{name}"...',
'toolbar.status.failedImportMidi': 'Failed to import MIDI file: {error}',
'toolbar.status.playbackFailedStart': 'Playback failed to start',
'toolbar.status.recordingStoppedCommitted': 'Recording stopped - notes committed',
'toolbar.status.failedStopPlayback': 'Failed to stop playback',
'toolbar.maxBars.prompt': 'Enter new max bars (>= {min}):',
'toolbar.input.invalidNumber': 'Invalid input. Please enter a valid number.',
'toolbar.maxBars.invalid': 'Invalid value. Please enter a number >= {min}.',
'toolbar.status.maxBarsChanged': 'Max bars changed to {value}',
'toolbar.bpm.prompt': 'Enter new BPM ({min}-{max}):',
'toolbar.bpm.invalid': 'Invalid BPM. Please enter a value between {min} and {max}.',
'toolbar.status.bpmChanged': 'BPM changed to {value}',
'toolbar.timeSignature.prompt': 'Set the time signature:',
'toolbar.status.timeSignatureChanged': 'Time signature changed to {value}',
'toolbar.status.keySignatureChanged': 'Key signature changed to {value}',
'toolbar.status.copied': 'Items copied to clipboard',
'toolbar.status.copyNone': 'No items selected to copy',
'toolbar.status.pasted': 'Items pasted from clipboard',
'toolbar.status.pasteFailed': 'Cannot paste - no valid clipboard content or context',
'toolbar.status.deleted': 'Selected regions deleted',
'toolbar.status.deleteNone': 'No regions selected for deletion',
'toolbar.undo.none': 'Nothing to undo',
'toolbar.redo.none': 'Nothing to redo',
'toolbar.status.undid': 'Undid: {description}',
'toolbar.status.redid': 'Redid: {description}',
'toolbar.action': 'action',
'toolbar.status.chatToggled': 'Chat toggled',
'toolbar.status.settingsToggled': 'Settings toggled',
'toolbar.pianoRoll.selectMidiRegion': 'Please select a MIDI region to open the Piano Roll.',
'toolbar.status.recordingStopped': 'Recording stopped',
'toolbar.status.audioRecordingStarted': 'Audio recording started...',
'toolbar.recording.openMidiRegion': 'Please open a MIDI region in the Piano Roll before starting recording.',
'toolbar.recording.selectMidiRegion': 'Please select a MIDI region before starting recording.',
'toolbar.recording.noMidiDevice': 'No MIDI device detected. Please connect a MIDI keyboard and try again.',
'toolbar.status.recordingStarted': 'Recording started...',
'toolbar.button.new': 'New',
'toolbar.button.load': 'Load',
'toolbar.button.save': 'Save',
'toolbar.button.export': 'Export',
'toolbar.button.import': 'Import',
'toolbar.button.settings': 'Settings',
'toolbar.button.undo': 'Undo',
'toolbar.button.redo': 'Redo',
'toolbar.button.select': 'Select',
'toolbar.button.pencil': 'Pencil',
'toolbar.button.splitRegion': 'Split Region at Playhead',
'toolbar.button.mergeRegions': 'Merge Selected MIDI Regions',
'toolbar.button.snapToGrid': 'Snap to Grid',
'toolbar.button.copy': 'Copy',
'toolbar.button.paste': 'Paste',
'toolbar.button.delete': 'Delete',
'toolbar.button.backToBeginning': 'Back to beginning',
'toolbar.button.play': 'Play',
'toolbar.button.pause': 'Pause',
'toolbar.button.stopRecording': 'Stop Recording',
'toolbar.button.record': 'Record',
'toolbar.button.loop': 'Loop',
'toolbar.button.metronome': 'Metronome',
'toolbar.button.piano': 'Piano',
'toolbar.button.kgone': 'K.G.One Music Generator',
'toolbar.button.chat': 'Chat',
'toolbar.button.eventList': 'Event List Editor',
'toolbar.keySignatureChooser': 'Choose key signature, current {value}',
'toolbar.export.label': 'Export',
'toolbar.importProject.title': 'Import Project',
'toolbar.importProject.description': 'Drag and drop your project file here',
'toolbar.openingProject': 'Opening project...',
'fileImport.invalidType': 'Invalid file type. Please select a file with one of these extensions: {extensions}',
'fileImport.close': 'Close import modal',
'fileImport.supportedFormats': 'Supported formats: {formats}',
'fileImport.or': 'or',
'fileImport.browse': 'Browse Files',
'instrument.group.PIANO_AND_KEYBOARDS': 'Piano and Keyboards',
'instrument.group.GUITAR': 'Guitar',
'instrument.group.BASS': 'Bass',
'instrument.group.STRINGS': 'Strings',
'instrument.group.BRASS': 'Brass',
'instrument.group.WOODWIND': 'Woodwind',
'instrument.group.PERCUSSION_KIT': 'Percussion Kit',
'instrument.group.SYNTH': 'Synthesizer',
'instrument.name.acoustic_grand_piano': 'Acoustic Grand Piano',
'instrument.name.bright_acoustic_piano': 'Bright Acoustic Piano',
'instrument.name.electric_grand_piano': 'Electric Grand Piano',
'instrument.name.electric_piano_1': 'Electric Piano 1',
'instrument.name.electric_piano_2': 'Electric Piano 2',
'instrument.name.drawbar_organ': 'Drawbar Organ',
'instrument.name.clavinet': 'Clavinet',
'instrument.name.harpsichord': 'Harpsichord',
'instrument.name.accordion': 'Accordion',
'instrument.name.acoustic_guitar_nylon': 'Acoustic Guitar (nylon)',
'instrument.name.acoustic_guitar_steel': 'Acoustic Guitar (steel)',
'instrument.name.electric_guitar_clean': 'Electric Guitar (clean)',
'instrument.name.overdriven_guitar': 'Electric Guitar (overdrive)',
'instrument.name.distortion_guitar': 'Electric Guitar (distortion)',
'instrument.name.acoustic_bass': 'Acoustic Bass',
'instrument.name.electric_bass_finger': 'Electric Bass (finger)',
'instrument.name.electric_bass_pick': 'Electric Bass (picked)',
'instrument.name.slap_bass_1': 'Slap Bass',
'instrument.name.synth_bass_1': 'Synth Bass 1',
'instrument.name.violin': 'Violin',
'instrument.name.viola': 'Viola',
'instrument.name.cello': 'Cello',
'instrument.name.contrabass': 'Contrabass',
'instrument.name.string_ensemble_1': 'String Ensemble',
'instrument.name.string_ensemble_2': 'String Ensemble 2',
'instrument.name.pizzicato_strings': 'Pizzicato Strings',
'instrument.name.tremolo_strings': 'Tremolo Strings',
'instrument.name.orchestral_harp': 'Orchestral Harp',
'instrument.name.trumpet': 'Trumpet',
'instrument.name.trombone': 'Trombone',
'instrument.name.tuba': 'Tuba',
'instrument.name.french_horn': 'French Horn',
'instrument.name.brass_section': 'Brass Section',
'instrument.name.soprano_sax': 'Soprano Sax',
'instrument.name.alto_sax': 'Alto Sax',
'instrument.name.tenor_sax': 'Tenor Sax',
'instrument.name.baritone_sax': 'Baritone Sax',
'instrument.name.oboe': 'Oboe',
'instrument.name.english_horn': 'English Horn',
'instrument.name.bassoon': 'Bassoon',
'instrument.name.clarinet': 'Clarinet',
'instrument.name.piccolo': 'Piccolo',
'instrument.name.flute': 'Flute',
'instrument.name.shakuhachi': 'Shakuhachi',
'instrument.name.harmonica': 'Harmonica',
'instrument.name.standard': 'Standard Drum Kit',
'instrument.name.orchestra_kit': 'Orchestra Drum Kit',
'instrument.name.marimba': 'Marimba',
'instrument.name.timpani': 'Timpani',
'instrument.name.taiko_drum': 'Taiko Drum',
'instrument.name.woodblock': 'Woodblock',
'instrument.name.lead_1_square': 'Lead 1 (square)',
'instrument.name.lead_2_sawtooth': 'Lead 2 (sawtooth)',
'instrument.name.pad_1_new_age': 'Pad 1 (new age)',
'instrument.name.pad_2_warm': 'Pad 2 (warm)',
'instrument.name.pad_3_polysynth': 'Pad 3 (polysynth)',
'instrument.name.pad_4_choir': 'Pad 4 (choir)',
'instrument.name.pad_5_bowed': 'Pad 5 (bowed glass)',
'instrument.name.pad_6_metallic': 'Pad 6 (metallic)',
'instrument.name.pad_7_halo': 'Pad 7 (halo)',
'instrument.name.pad_8_sweep': 'Pad 8 (sweep)',
'instrument.name.fx_1_rain': 'FX 1 (rain)',
'instrument.name.fx_2_soundtrack': 'FX 2 (soundtrack)',
'instrument.name.fx_3_crystal': 'FX 3 (crystal)',
'instrument.name.fx_4_atmosphere': 'FX 4 (atmosphere)',
'instrument.name.fx_5_brightness': 'FX 5 (brightness)',
'instrument.name.fx_6_goblins': 'FX 6 (goblins)',
'instrument.name.fx_7_echoes': 'FX 7 (echoes)',
'instrument.name.fx_8_scifi': 'FX 8 (sci-fi)',
};
+8
View File
@@ -0,0 +1,8 @@
import type { ResolvedLocaleCode, TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
import { zhCnMessages } from './zh_cn';
export const messagesByLocale: Record<ResolvedLocaleCode, TranslationMessages> = {
en_us: enUsMessages,
zh_cn: zhCnMessages,
};
+510
View File
@@ -0,0 +1,510 @@
import type { TranslationMessages } from '../types';
export const zhCnMessages: TranslationMessages = {
'app.loading': '加载中...',
'status.chordGuideCandidate': '和弦指导候选: {name} - {notes} - {note}',
'settings.sidebar.title': '设置',
'settings.sidebar.close': '关闭设置',
'settings.sidebar.general': '通用',
'settings.sidebar.audioIo': '音频 I/O',
'settings.sidebar.behavior': '行为',
'settings.sidebar.templates': '模板',
'settings.sidebar.chordGuide': '和弦指导',
'settings.general.title': '通用',
'settings.general.language.label': '语言',
'settings.general.language.help': '自动会使用你的系统语言。',
'settings.general.language.auto': '自动',
'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': '简体中文',
'settings.yes': '是',
'settings.no': '否',
'settings.restoreDefault': '恢复默认',
'settings.deleteCachedModel': '删除已缓存模型',
'settings.deleting': '删除中...',
'settings.general.llmProvider.section': 'LLM 提供方',
'settings.general.llmProvider.label': 'LLM 提供方',
'settings.general.llmProvider.local': '本地 LLM(浏览器)',
'settings.general.llmProvider.openai': 'OpenAI',
'settings.general.llmProvider.claudeOpenRouter': 'Claude(通过 OpenRouter',
'settings.general.llmProvider.openaiCompatible': 'OpenAI 兼容服务(例如 OpenRouter、Ollama',
'settings.general.persistKeys.label': '在非 localhost 环境持久化 API Key',
'settings.general.persistKeys.help': '启用后,即使在非 localhost 环境下,API Key 也会保存到浏览器存储中。警告:这可能增加 XSS 攻击带来的安全风险。',
'settings.general.localRuntime.cachedStatus': '缓存模型状态',
'settings.general.localRuntime.cacheChecking': '正在检查本地模型缓存...',
'settings.general.localRuntime.cacheDownloaded': '已下载到浏览器缓存。',
'settings.general.localRuntime.cacheMissing': '尚未下载。',
'settings.general.localRuntime.contextLength': '上下文长度',
'settings.general.localRuntime.contextHelp': '更大的上下文长度需要更多显存,并且在对话变长后也可能降低性能。',
'settings.general.localRuntime.downloadUrl': '下载地址',
'settings.general.localRuntime.downloadHelp': '修改这个地址可能导致下载失败,或指向不兼容的模型文件。',
'settings.general.localRuntime.autoDownload': '下次你使用 `Local LLM (Browser)` 聊天时,本地模型会自动下载。',
'settings.general.uvr5.section': 'UVR5 Web 运行时',
'settings.general.uvr5.downloadUrl': 'UVR-MDX-NET-Inst_HQ_3 下载地址',
'settings.general.htdemucs.downloadUrl': 'htdemucs_4s 下载地址',
'settings.general.modelUrl.help': '修改这个地址可能导致下载失败,或指向不兼容的模型文件。',
'settings.general.openai.section': 'OpenAI',
'settings.general.openai.key': '密钥',
'settings.general.openai.keyPlaceholder': '输入你的 OpenAI API Key',
'settings.general.openai.model': '模型',
'settings.general.openai.flexMode': 'Flex 模式',
'settings.general.openai.flexHelp': 'Flex 模式使用 OpenAI 的弹性服务层。优点:繁忙时段可能节省成本并提高吞吐。缺点:延迟可能波动,也可能排队或被降优先级。仅对 OpenAI 提供方生效,对 OpenAI 兼容服务无影响。',
'settings.general.keys.persisted': '密钥会保存在本地(浏览器中的 IndexedDB)。',
'settings.general.keys.sessionOnly': '出于安全考虑,在非本地主机环境下密钥不会持久化,只保留在当前会话内存中。',
'settings.general.claudeOpenRouter.section': 'Anthropic Claude(通过 OpenRouter',
'settings.general.claudeOpenRouter.keyPlaceholder': '输入你的 Claude API Key',
'settings.general.baseUrl': '基础 URL',
'settings.general.claudeOpenRouter.baseUrlHelp': '这是 OpenRouter API 的基础 URL。除非你明确知道自己在做什么,否则不要修改。',
'settings.general.openaiCompatible.section': 'OpenAI 兼容服务',
'settings.general.openaiCompatible.keyPlaceholder': '输入你的 API Key',
'settings.general.openaiCompatible.baseUrlPlaceholder': '例如 https://openrouter.ai/api/v1',
'settings.general.openaiCompatible.baseUrlHelp': '快捷预设:',
'settings.general.openaiCompatible.modelPlaceholder': '例如 qwen3:30b',
'settings.general.soundfont.section': 'Soundfont 设置',
'settings.general.soundfont.managed': 'Soundfont 配置由服务器(kgone-server.json)管理,当前设置为只读。',
'settings.general.soundfont.baseUrl': '基础 URL',
'settings.general.soundfont.baseUrlHelp': '如果改成不兼容的 soundfont 源,某些乐器可能发声错误或无法播放。',
'settings.general.kgone.section': 'K.G.One 设置',
'settings.general.kgone.managed': 'K.G.One 配置由服务器(kgone-server.json)管理,当前设置为只读。',
'settings.general.kgone.enabled': '启用 K.G.One 集成',
'settings.general.kgone.disabled': '禁用',
'settings.general.kgone.enabledOption': '启用',
'settings.general.kgone.serverBaseUrl': '服务器基础 URL',
'settings.general.kgone.serverBaseUrlHelp': '正在运行的 K.G.One Music Studio 服务器基础 URL。用于整曲生成、片段生成和分轨。',
'dialog.title.notice': '提示',
'dialog.title.timeSignature': '拍号',
'dialog.title.tempoDetection': '速度检测',
'dialog.title.applyTempo': '应用速度',
'dialog.title.chordDetection': '和弦检测',
'dialog.title.input': '输入',
'dialog.title.confirm': '确认',
'dialog.close': '关闭对话框',
'dialog.cancel': '取消',
'dialog.ok': '确定',
'dialog.experimentalFeature': '实验性功能',
'dialog.recommendedSource': '推荐源素材',
'dialog.chordHint.audio': '和弦分析仍属实验性功能。和声内容、编配密度以及瞬态较强的素材都可能影响准确率。建议先使用默认设置,再逐步微调灵敏度和稳定性,让检测结果更贴近实际乐句。',
'dialog.chordHint.midi': '和弦分析仍属实验性功能。和弦堆叠密度、重叠以及装饰音都会影响结果。建议先使用默认设置,再调整短音抑制和和声关注方式,以匹配该段音乐的和声作用。',
'dialog.chordHint.tempo': '速度分析仍属实验性功能。自由速度、瞬态稀疏以及多层打击乐都可能降低准确率。建议先使用默认 BPM 范围,如果第一次结果不理想,再把分析范围收窄到更可能的速度区间。',
'dialog.sourceHint.kgone': '为了获得更可靠的和弦标签,建议分析已经削弱或移除了人声和打击乐的 stem。如果已启用 K.G.One Music Studio 服务器集成,请使用带有 “Vocal, Drums, Bass, Guitar, Piano, and Others” 的 Separator 模型,并优先分析 Piano 或 Others stem。',
'dialog.sourceHint.local': '为了获得更可靠的和弦标签,建议分析已经削弱或移除了人声和打击乐的 stem。如果你使用本地分轨器,请选择 “Vocal, Drums, Bass, and Others” 模型,并使用 Others stem 进行分析。',
'dialog.label.sensitivity': '灵敏度',
'dialog.label.stability': '稳定性',
'dialog.label.noChordThreshold': '无和弦阈值',
'dialog.label.enableSevenths': '和弦细节:启用七和弦',
'dialog.label.shortNotes': '短音',
'dialog.option.suppressionLow': '低抑制',
'dialog.option.suppressionMedium': '中抑制',
'dialog.option.suppressionHigh': '高抑制',
'dialog.label.harmonicFocus': '和声关注',
'dialog.option.harmonicBalanced': '均衡',
'dialog.option.harmonicSustained': '偏向持续音',
'dialog.label.minimumBpm': '最小 BPM',
'dialog.label.maximumBpm': '最大 BPM',
'dialog.label.autoAlignRegionToBeat': '自动将片段对齐到拍点',
'eventList.title': '事件列表',
'eventList.scopeTabs': '事件列表范围',
'eventList.scope.region': '区域',
'eventList.scope.track': '音轨',
'eventList.scope.global': '全局',
'eventList.deleteVisibleSelectedRows': '删除当前可见的已选行',
'eventList.table.position': '位置',
'eventList.table.status': '状态',
'eventList.table.num': '编号',
'eventList.table.val': '数值',
'eventList.table.lengthInfo': '长度/信息',
'eventList.region.filters': '区域事件筛选',
'eventList.region.filter.notes': '音符',
'eventList.region.filter.pitchBends': '弯音',
'eventList.region.filter.controller': '控制器',
'eventList.region.empty': '请选择一个 MIDI 区域,或先在钢琴卷帘中打开一个区域,再查看事件列表。',
'eventList.region.add.label': '添加',
'eventList.region.add.noteTitle': '在播放头位置添加音符',
'eventList.region.add.pitchBendTitle': '在播放头位置添加弯音',
'eventList.region.add.controllerTitle': '在播放头位置添加控制器事件',
'eventList.region.addType.note': '音符',
'eventList.region.addType.pitchBend': '弯音',
'eventList.region.addType.controller': '控制器',
'eventList.region.status.note': '音符',
'eventList.region.status.pitchBend': '弯音',
'eventList.region.status.controller': '控制器',
'eventList.track.filters': '音轨列表模式',
'eventList.track.filter.regions': '区域',
'eventList.track.filter.volume': '音量',
'eventList.track.filter.pan': '声像',
'eventList.track.empty': '请选择一条音轨以查看区域和音轨自动化。',
'eventList.track.add.label': '添加',
'eventList.track.add.midiRegionTitle': '在播放头位置添加 MIDI 区域',
'eventList.track.add.volumeTitle': '在播放头位置添加音量自动化点',
'eventList.track.add.panTitle': '在播放头位置添加声像自动化点',
'eventList.track.addType.midiRegion': 'MIDI 区域',
'eventList.track.status.audio': '音频',
'eventList.track.status.midi': 'MIDI',
'eventList.track.status.volume': '音量',
'eventList.track.status.pan': '声像',
'eventList.global.filters': '全局事件筛选',
'eventList.global.filter.marker': '标记',
'eventList.global.filter.tempo': '速度',
'eventList.global.filter.keySignature': '调号',
'eventList.global.filter.chord': '和弦',
'eventList.global.add.label': '添加',
'eventList.global.add.markerTitle': '在播放头位置添加标记区段',
'eventList.global.add.tempoTitle': '在播放头位置添加速度区段',
'eventList.global.add.keySignatureTitle': '在播放头位置添加调号区段',
'eventList.global.add.chordTitle': '在播放头位置添加和弦区段',
'eventList.global.addType.marker': '标记',
'eventList.global.addType.tempo': '速度',
'eventList.global.addType.keySignature': '调号',
'eventList.global.addType.chord': '和弦',
'eventList.global.status.marker': '标记',
'eventList.global.status.tempo': '速度',
'eventList.global.status.keySignature': '调号',
'eventList.global.status.chord': '和弦',
'eventList.global.validation.marker': '请输入标记标签。需要非空文本标签。示例:前奏',
'eventList.global.validation.tempo': '请输入仅包含数字的 BPM 值。需要介于 {min} 和 {max} 之间的整数。示例:128',
'eventList.global.validation.keySignature': '请输入一个精确的调号名称。需要规范值,例如 “C major” 或 “F# minor”。示例:F# minor',
'eventList.global.validation.chord': '请输入有效的和弦符号。需要应用可解析的和弦表示。示例:Bm7b5',
'eventList.global.validation.position': '请输入项目起点或之后的位置。需要非负位置。示例:1 1 0',
'eventList.global.validation.length': '请输入正长度。需要大于零的时值。示例:4 0',
'globalTracks.label': '全局轨道',
'globalTracks.marker': '标记',
'globalTracks.tempo': '速度',
'globalTracks.signature': '调号',
'globalTracks.chord': '和弦',
'globalTracks.addItem': '添加 {label} 全局轨道项目',
'pianoRoll.power.linear': '线性',
'pianoRoll.power.sqrtDefault': '√(默认)',
'pianoRoll.power.mild': '轻度',
'pianoRoll.power.strong': '强烈',
'pianoRoll.chordGuide.off': '关闭和弦指导',
'pianoRoll.chordGuide.tonic': '主功能和弦指导',
'pianoRoll.chordGuide.subdominant': '下属功能和弦指导',
'pianoRoll.chordGuide.dominant': '属功能和弦指导',
'pianoRoll.spectrogramView': '频谱视图',
'pianoRoll.sheetMusicView': '五线谱视图',
'pianoRoll.pointerTool': '指针工具',
'pianoRoll.pencilTool': '铅笔工具',
'pianoRoll.toggleAutomationLane': '切换自动化轨',
'pianoRoll.automation': '自动化',
'pianoRoll.automationLane': '{label} 自动化轨',
'pianoRoll.automationType.pitchBend': '弯音',
'pianoRoll.automationType.cc1': 'CC1',
'pianoRoll.automationType.cc2': 'CC2',
'pianoRoll.automationType.cc7': 'CC7',
'pianoRoll.automationType.cc11': 'CC11',
'pianoRoll.automationType.cc64': 'CC64',
'pianoRoll.mode': '调式',
'pianoRoll.modeOption.ionian': '伊奥尼亚',
'pianoRoll.modeOption.dorian': '多利亚',
'pianoRoll.modeOption.phrygian': '弗里吉亚',
'pianoRoll.modeOption.lydian': '利底亚',
'pianoRoll.modeOption.mixolydian': '混合利底亚',
'pianoRoll.modeOption.aeolian': '爱奥利亚',
'pianoRoll.modeOption.locrian': '洛克里亚',
'pianoRoll.modeOption.harmonic_minor': '和声小调',
'pianoRoll.modeOption.melodic_minor': '旋律小调',
'pianoRoll.modeOption.phrygian_dominant': '弗里吉亚属调式',
'pianoRoll.modeOption.harmonic_major': '和声大调',
'pianoRoll.chordGuide': '和弦指导',
'pianoRoll.sheetQuantization': '谱面量化',
'pianoRoll.snap': '吸附',
'pianoRoll.snap.none': '无吸附',
'pianoRoll.quantizePositionCompact': '位置量化',
'pianoRoll.quantizeLengthCompact': '长度量化',
'pianoRoll.floor': '底噪',
'pianoRoll.curve': '曲线',
'pianoRoll.zoom': '缩放',
'pianoRoll.moreOptions': '更多选项',
'pianoRoll.quantize.1/1': '1/1',
'pianoRoll.quantize.1/2': '1/2',
'pianoRoll.quantize.1/3': '1/3',
'pianoRoll.quantize.1/4': '1/4',
'pianoRoll.quantize.1/6': '1/6',
'pianoRoll.quantize.1/8': '1/8',
'pianoRoll.quantize.1/12': '1/12',
'pianoRoll.quantize.1/16': '1/16',
'pianoRoll.quantize.1/24': '1/24',
'pianoRoll.quantize.1/32': '1/32',
'pianoRoll.showActiveRegionOnly': '仅显示当前区域',
'pianoRoll.showEntireTrack': '显示整条音轨',
'pianoRoll.detectChords': '检测和弦...',
'pianoRoll.detectingChords': '正在检测和弦...',
'pianoRoll.detectTempo': '检测速度...',
'pianoRoll.detectingTempo': '正在检测速度...',
'percussion.short.35': '原底鼓',
'percussion.short.36': '底鼓',
'percussion.short.37': '边击',
'percussion.short.38': '原军鼓',
'percussion.short.39': '拍手',
'percussion.short.40': '电军鼓',
'percussion.short.41': '低落地鼓',
'percussion.short.42': '闭镲',
'percussion.short.43': '高落地鼓',
'percussion.short.44': '踏镲',
'percussion.short.45': '低嗵鼓',
'percussion.short.46': '开镲',
'percussion.short.47': '中低嗵鼓',
'percussion.short.48': '中高嗵鼓',
'percussion.short.49': '碎音镲1',
'percussion.short.50': '高嗵鼓',
'percussion.short.51': '叮叮镲1',
'percussion.short.52': '中国镲',
'percussion.short.53': '叮叮镲铃',
'percussion.short.54': '铃鼓',
'percussion.short.55': '飞溅镲',
'percussion.short.56': '牛铃',
'percussion.short.57': '碎音镲2',
'percussion.short.58': '振响器',
'percussion.short.59': '叮叮镲2',
'percussion.short.60': '高邦戈',
'percussion.short.61': '低邦戈',
'percussion.short.62': '闷高康加',
'percussion.short.63': '开高康加',
'percussion.short.64': '低康加',
'percussion.short.65': '高廷巴勒',
'percussion.short.66': '低廷巴勒',
'percussion.short.67': '高阿哥哥',
'percussion.short.68': '低阿哥哥',
'percussion.short.69': '卡巴萨',
'percussion.short.70': '沙锤',
'percussion.short.71': '短哨',
'percussion.short.72': '长哨',
'percussion.short.73': '短刮瓜',
'percussion.short.74': '长刮瓜',
'percussion.short.75': '响棒',
'percussion.short.76': '高木块',
'percussion.short.77': '低木块',
'percussion.short.78': '闷库卡',
'percussion.short.79': '开库卡',
'percussion.short.80': '闷三角铁',
'percussion.short.81': '开三角铁',
'percussion.full.35': '原声底鼓',
'percussion.full.36': '底鼓 1',
'percussion.full.37': '边击',
'percussion.full.38': '原声军鼓',
'percussion.full.39': '拍手',
'percussion.full.40': '电军鼓',
'percussion.full.41': '低落地嗵鼓',
'percussion.full.42': '闭合踩镲',
'percussion.full.43': '高落地嗵鼓',
'percussion.full.44': '踏踩镲',
'percussion.full.45': '低嗵鼓',
'percussion.full.46': '开放踩镲',
'percussion.full.47': '中低嗵鼓',
'percussion.full.48': '中高嗵鼓',
'percussion.full.49': '碎音镲 1',
'percussion.full.50': '高嗵鼓',
'percussion.full.51': '叮叮镲 1',
'percussion.full.52': '中国镲',
'percussion.full.53': '叮叮镲铃',
'percussion.full.54': '铃鼓',
'percussion.full.55': '飞溅镲',
'percussion.full.56': '牛铃',
'percussion.full.57': '碎音镲 2',
'percussion.full.58': '振响器',
'percussion.full.59': '叮叮镲 2',
'percussion.full.60': '高邦戈',
'percussion.full.61': '低邦戈',
'percussion.full.62': '闷高康加',
'percussion.full.63': '开高康加',
'percussion.full.64': '低康加',
'percussion.full.65': '高廷巴勒',
'percussion.full.66': '低廷巴勒',
'percussion.full.67': '高阿哥哥',
'percussion.full.68': '低阿哥哥',
'percussion.full.69': '卡巴萨',
'percussion.full.70': '沙锤',
'percussion.full.71': '短哨',
'percussion.full.72': '长哨',
'percussion.full.73': '短刮瓜',
'percussion.full.74': '长刮瓜',
'percussion.full.75': '响棒',
'percussion.full.76': '高木块',
'percussion.full.77': '低木块',
'percussion.full.78': '闷库卡',
'percussion.full.79': '开库卡',
'percussion.full.80': '闷三角铁',
'percussion.full.81': '开三角铁',
'toolbar.export.kgstudio': '导出为 KGStudio 文件',
'toolbar.export.midi': '导出为 MIDI 文件',
'toolbar.export.wav': '导出为 WAV',
'toolbar.export.mp3': '导出为 MP3',
'toolbar.projectName.prompt': '输入项目名称:',
'toolbar.projectName.invalid': '无效的项目名称。只允许字母、数字、空格、连字符、下划线、句点和括号。',
'toolbar.projectName.reserved': '"{name}" 是保留项目名称。请选择其他名称。',
'toolbar.projectName.overwrite': '项目 “{name}” 已存在。要覆盖它吗?',
'toolbar.projectName.renameOrCopy': '你想重命名这个项目,还是另存为一个副本?',
'toolbar.projectName.saveAsCopy': '另存为副本',
'toolbar.projectName.rename': '重命名',
'toolbar.save.error': '保存时发生错误:{error}',
'toolbar.load.error': '加载项目时发生错误:{error}',
'toolbar.newProject.confirm': '确定要创建新项目吗?当前未保存的更改将会丢失。',
'toolbar.project.notFound': '未找到项目 “{name}”。',
'toolbar.openProject.confirm': '要打开这个项目吗?当前项目中未保存的更改将会丢失。',
'toolbar.status.loadFailed': '加载项目失败:{error}',
'toolbar.status.newProjectCreated': '已创建新项目 “{name}”',
'toolbar.status.projectLoaded': '{description} 加载成功',
'toolbar.status.projectExportedKgstudio': '项目 “{name}” 已导出为 KGStudio 文件',
'toolbar.status.exportProjectError': '导出项目出错:{error}',
'toolbar.export.failedProject': '导出项目失败:{error}',
'toolbar.status.projectExportedMidi': '项目 “{name}” 已导出为 MIDI 文件',
'toolbar.status.exportMidiError': '导出 MIDI 出错:{error}',
'toolbar.export.failedMidi': '导出项目为 MIDI 失败:{error}',
'toolbar.status.projectExportedWav': '项目 “{name}” 已导出为 WAV 文件',
'toolbar.status.exportWavError': '导出 WAV 出错:{error}',
'toolbar.export.failedWav': '导出项目为 WAV 失败:{error}',
'toolbar.status.projectExportedMp3': '项目 “{name}” 已导出为 MP3 文件',
'toolbar.status.exportMp3Error': '导出 MP3 出错:{error}',
'toolbar.export.failedMp3': '导出项目为 MP3 失败:{error}',
'toolbar.status.importFailed': '导入文件失败:{error}',
'toolbar.import.failedProjectFile': '导入项目文件失败:{error}',
'toolbar.import.corruptedKgstudio': '.kgstudio 文件已损坏或无效:{error}',
'toolbar.status.importingMidi': '正在导入 MIDI 文件 “{name}”...',
'toolbar.status.failedImportMidi': '导入 MIDI 文件失败:{error}',
'toolbar.status.playbackFailedStart': '播放启动失败',
'toolbar.status.recordingStoppedCommitted': '录音已停止 - 音符已提交',
'toolbar.status.failedStopPlayback': '停止播放失败',
'toolbar.maxBars.prompt': '输入新的最大小节数(>= {min}):',
'toolbar.input.invalidNumber': '输入无效。请输入有效数字。',
'toolbar.maxBars.invalid': '数值无效。请输入一个 >= {min} 的数字。',
'toolbar.status.maxBarsChanged': '最大小节数已改为 {value}',
'toolbar.bpm.prompt': '输入新的 BPM{min}-{max}):',
'toolbar.bpm.invalid': 'BPM 无效。请输入 {min} 到 {max} 之间的值。',
'toolbar.status.bpmChanged': 'BPM 已改为 {value}',
'toolbar.timeSignature.prompt': '设置拍号:',
'toolbar.status.timeSignatureChanged': '拍号已改为 {value}',
'toolbar.status.keySignatureChanged': '调号已改为 {value}',
'toolbar.status.copied': '项目已复制到剪贴板',
'toolbar.status.copyNone': '没有可复制的已选项目',
'toolbar.status.pasted': '已从剪贴板粘贴项目',
'toolbar.status.pasteFailed': '无法粘贴 - 没有有效的剪贴板内容或上下文',
'toolbar.status.deleted': '已删除所选区域',
'toolbar.status.deleteNone': '没有选中可删除的区域',
'toolbar.undo.none': '没有可撤销的操作',
'toolbar.redo.none': '没有可重做的操作',
'toolbar.status.undid': '已撤销:{description}',
'toolbar.status.redid': '已重做:{description}',
'toolbar.action': '操作',
'toolbar.status.chatToggled': '聊天窗口已切换',
'toolbar.status.settingsToggled': '设置面板已切换',
'toolbar.pianoRoll.selectMidiRegion': '请先选择一个 MIDI 区域再打开钢琴卷帘。',
'toolbar.status.recordingStopped': '录音已停止',
'toolbar.status.audioRecordingStarted': '音频录音已开始...',
'toolbar.recording.openMidiRegion': '开始录音前,请先在钢琴卷帘中打开一个 MIDI 区域。',
'toolbar.recording.selectMidiRegion': '开始录音前,请先选择一个 MIDI 区域。',
'toolbar.recording.noMidiDevice': '未检测到 MIDI 设备。请连接 MIDI 键盘后重试。',
'toolbar.status.recordingStarted': '录音已开始...',
'toolbar.button.new': '新建',
'toolbar.button.load': '打开',
'toolbar.button.save': '保存',
'toolbar.button.export': '导出',
'toolbar.button.import': '导入',
'toolbar.button.settings': '设置',
'toolbar.button.undo': '撤销',
'toolbar.button.redo': '重做',
'toolbar.button.select': '选择',
'toolbar.button.pencil': '铅笔',
'toolbar.button.splitRegion': '在播放头处分割区域',
'toolbar.button.mergeRegions': '合并选中的 MIDI 区域',
'toolbar.button.snapToGrid': '吸附到网格',
'toolbar.button.copy': '复制',
'toolbar.button.paste': '粘贴',
'toolbar.button.delete': '删除',
'toolbar.button.backToBeginning': '回到开头',
'toolbar.button.play': '播放',
'toolbar.button.pause': '暂停',
'toolbar.button.stopRecording': '停止录音',
'toolbar.button.record': '录音',
'toolbar.button.loop': '循环',
'toolbar.button.metronome': '节拍器',
'toolbar.button.piano': '钢琴',
'toolbar.button.kgone': 'K.G.One 音乐生成器',
'toolbar.button.chat': '聊天',
'toolbar.button.eventList': '事件列表编辑器',
'toolbar.keySignatureChooser': '选择调号,当前为 {value}',
'toolbar.export.label': '导出',
'toolbar.importProject.title': '导入项目',
'toolbar.importProject.description': '将项目文件拖放到这里',
'toolbar.openingProject': '正在打开项目...',
'fileImport.invalidType': '文件类型无效。请选择以下扩展名之一的文件:{extensions}',
'fileImport.close': '关闭导入弹窗',
'fileImport.supportedFormats': '支持的格式:{formats}',
'fileImport.or': '或',
'fileImport.browse': '浏览文件',
'instrument.group.PIANO_AND_KEYBOARDS': '钢琴与键盘',
'instrument.group.GUITAR': '吉他',
'instrument.group.BASS': '贝斯',
'instrument.group.STRINGS': '弦乐',
'instrument.group.BRASS': '铜管',
'instrument.group.WOODWIND': '木管',
'instrument.group.PERCUSSION_KIT': '打击乐套件',
'instrument.group.SYNTH': '合成器',
'instrument.name.acoustic_grand_piano': '原声大钢琴',
'instrument.name.bright_acoustic_piano': '明亮原声钢琴',
'instrument.name.electric_grand_piano': '电大钢琴',
'instrument.name.electric_piano_1': '电钢琴 1',
'instrument.name.electric_piano_2': '电钢琴 2',
'instrument.name.drawbar_organ': '击杆风琴',
'instrument.name.clavinet': '击弦电钢琴',
'instrument.name.harpsichord': '羽管键琴',
'instrument.name.accordion': '手风琴',
'instrument.name.acoustic_guitar_nylon': '原声吉他(尼龙弦)',
'instrument.name.acoustic_guitar_steel': '原声吉他(钢弦)',
'instrument.name.electric_guitar_clean': '电吉他(清音)',
'instrument.name.overdriven_guitar': '电吉他(过载)',
'instrument.name.distortion_guitar': '电吉他(失真)',
'instrument.name.acoustic_bass': '原声贝斯',
'instrument.name.electric_bass_finger': '电贝斯(指弹)',
'instrument.name.electric_bass_pick': '电贝斯(拨片)',
'instrument.name.slap_bass_1': '击勾贝斯',
'instrument.name.synth_bass_1': '合成贝斯 1',
'instrument.name.violin': '小提琴',
'instrument.name.viola': '中提琴',
'instrument.name.cello': '大提琴',
'instrument.name.contrabass': '低音提琴',
'instrument.name.string_ensemble_1': '弦乐合奏',
'instrument.name.string_ensemble_2': '弦乐合奏 2',
'instrument.name.pizzicato_strings': '拨奏弦乐',
'instrument.name.tremolo_strings': '震音弦乐',
'instrument.name.orchestral_harp': '管弦竖琴',
'instrument.name.trumpet': '小号',
'instrument.name.trombone': '长号',
'instrument.name.tuba': '大号',
'instrument.name.french_horn': '圆号',
'instrument.name.brass_section': '铜管组',
'instrument.name.soprano_sax': '高音萨克斯',
'instrument.name.alto_sax': '中音萨克斯',
'instrument.name.tenor_sax': '次中音萨克斯',
'instrument.name.baritone_sax': '上低音萨克斯',
'instrument.name.oboe': '双簧管',
'instrument.name.english_horn': '英国管',
'instrument.name.bassoon': '巴松',
'instrument.name.clarinet': '单簧管',
'instrument.name.piccolo': '短笛',
'instrument.name.flute': '长笛',
'instrument.name.shakuhachi': '尺八',
'instrument.name.harmonica': '口琴',
'instrument.name.standard': '标准鼓组',
'instrument.name.orchestra_kit': '管弦打击乐组',
'instrument.name.marimba': '马林巴',
'instrument.name.timpani': '定音鼓',
'instrument.name.taiko_drum': '太鼓',
'instrument.name.woodblock': '木鱼',
'instrument.name.lead_1_square': '主音 1(方波)',
'instrument.name.lead_2_sawtooth': '主音 2(锯齿波)',
'instrument.name.pad_1_new_age': '铺底 1(新世纪)',
'instrument.name.pad_2_warm': '铺底 2(温暖)',
'instrument.name.pad_3_polysynth': '铺底 3(复音合成)',
'instrument.name.pad_4_choir': '铺底 4(合唱)',
'instrument.name.pad_5_bowed': '铺底 5(弓弦玻璃)',
'instrument.name.pad_6_metallic': '铺底 6(金属)',
'instrument.name.pad_7_halo': '铺底 7(光晕)',
'instrument.name.pad_8_sweep': '铺底 8(扫频)',
'instrument.name.fx_1_rain': '特效 1(雨)',
'instrument.name.fx_2_soundtrack': '特效 2(配乐)',
'instrument.name.fx_3_crystal': '特效 3(水晶)',
'instrument.name.fx_4_atmosphere': '特效 4(氛围)',
'instrument.name.fx_5_brightness': '特效 5(明亮)',
'instrument.name.fx_6_goblins': '特效 6(妖精)',
'instrument.name.fx_7_echoes': '特效 7(回声)',
'instrument.name.fx_8_scifi': '特效 8(科幻)',
};
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest';
import { getPercussionKeyFullLabel, getPercussionKeyShortLabel, isGmDrumKitInstrument } from './percussion';
import { translate } from './translate';
describe('percussion i18n helpers', () => {
it('returns translated short labels for supported locales', () => {
expect(getPercussionKeyShortLabel(35, (key, params) => translate(key, params, 'en_us'))).toBe('Ac.Bass');
expect(getPercussionKeyShortLabel(35, (key, params) => translate(key, params, 'zh_cn'))).toBe('原底鼓');
});
it('returns translated full labels when available', () => {
expect(getPercussionKeyFullLabel(42, (key, params) => translate(key, params, 'en_us'))).toBe('Closed Hi Hat');
expect(getPercussionKeyFullLabel(42, (key, params) => translate(key, params, 'zh_cn'))).toBe('闭合踩镲');
});
it('falls back to English labels when translation is missing', () => {
expect(getPercussionKeyShortLabel(42, () => '')).toBe('ClosedHH');
expect(getPercussionKeyFullLabel(42, () => '')).toBe('Closed Hi Hat');
});
it('returns null for unknown pitches', () => {
expect(getPercussionKeyShortLabel(10, (key, params) => translate(key, params, 'en_us'))).toBeNull();
expect(getPercussionKeyFullLabel(10, (key, params) => translate(key, params, 'en_us'))).toBeNull();
});
it('keeps drum-kit eligibility explicit', () => {
expect(isGmDrumKitInstrument('standard')).toBe(true);
expect(isGmDrumKitInstrument('orchestra_kit')).toBe(true);
expect(isGmDrumKitInstrument('taiko_drum')).toBe(false);
});
});
+32
View File
@@ -0,0 +1,32 @@
import type { InstrumentType } from '../core/track/KGMidiTrack';
import { midiPercussionKeyMap } from '../util/midiUtil';
import type { TranslationParams } from './types';
type TranslateFn = (key: string, params?: TranslationParams) => string;
const GM_DRUM_KIT_INSTRUMENTS: ReadonlySet<InstrumentType> = new Set([
'standard',
'orchestra_kit',
]);
export function isGmDrumKitInstrument(instrument: InstrumentType): boolean {
return GM_DRUM_KIT_INSTRUMENTS.has(instrument);
}
export function getPercussionKeyShortLabel(pitch: number, t: TranslateFn): string | null {
const drumInfo = midiPercussionKeyMap[pitch];
if (!drumInfo) {
return null;
}
return t(`percussion.short.${pitch}`) || drumInfo.shortName;
}
export function getPercussionKeyFullLabel(pitch: number, t: TranslateFn): string | null {
const drumInfo = midiPercussionKeyMap[pitch];
if (!drumInfo) {
return null;
}
return t(`percussion.full.${pitch}`) || drumInfo.fullName;
}
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { translate } from './translate';
describe('translate', () => {
it('returns translated strings for both locales', () => {
expect(translate('settings.general.language.auto', undefined, 'en_us')).toBe('Auto');
expect(translate('settings.general.language.auto', undefined, 'zh_cn')).toBe('自动');
});
it('falls back to English when a zh-CN key is missing', () => {
expect(translate('toolbar.button.new', undefined, 'zh_cn')).toBe('新建');
expect(translate('nonexistent.key', undefined, 'zh_cn')).toBe('nonexistent.key');
});
it('interpolates params', () => {
expect(translate('toolbar.status.bpmChanged', { value: 120 }, 'en_us')).toBe('BPM changed to 120');
expect(translate('toolbar.status.bpmChanged', { value: 120 }, 'zh_cn')).toBe('BPM 已改为 120');
});
});
+38
View File
@@ -0,0 +1,38 @@
import { messagesByLocale } from './messages';
import type { ResolvedLocaleCode, TranslationMessages, TranslationParams } from './types';
let currentLocale: ResolvedLocaleCode = 'en_us';
function interpolate(template: string, params?: TranslationParams): string {
if (!params) {
return template;
}
return template.replace(/\{(\w+)\}/g, (_match, key: string) => {
const value = params[key];
return value === undefined ? `{${key}}` : String(value);
});
}
export function setCurrentLocale(locale: ResolvedLocaleCode): void {
currentLocale = locale;
}
export function getCurrentLocale(): ResolvedLocaleCode {
return currentLocale;
}
export function getMessagesForLocale(locale: ResolvedLocaleCode): TranslationMessages {
return messagesByLocale[locale];
}
export function translate(
key: string,
params?: TranslationParams,
locale: ResolvedLocaleCode = currentLocale,
): string {
const localeMessages = messagesByLocale[locale];
const englishMessages = messagesByLocale.en_us;
const template = localeMessages[key] ?? englishMessages[key] ?? key;
return interpolate(template, params);
}
+14
View File
@@ -0,0 +1,14 @@
export type LanguageSetting = 'auto' | 'en_us' | 'zh_cn';
export type ResolvedLocaleCode = Exclude<LanguageSetting, 'auto'>;
export type TranslationParams = Record<string, string | number>;
export type TranslationMessages = Record<string, string>;
export interface I18nContextValue {
languageSetting: LanguageSetting;
resolvedLocale: ResolvedLocaleCode;
setLanguageSetting: (value: LanguageSetting) => Promise<void>;
t: (key: string, params?: TranslationParams) => string;
}
+6
View File
@@ -0,0 +1,6 @@
import { useContext } from 'react';
import { I18nContext } from './I18nProvider';
export function useI18n() {
return useContext(I18nContext);
}