feat: added French support

This commit is contained in:
Xiaohan-Tian
2026-05-30 23:37:51 -07:00
parent 4c82f514d7
commit e263b1cc41
25 changed files with 862 additions and 29 deletions
+8 -1
View File
@@ -3,6 +3,7 @@ import { beforeAll, describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import ChatBox from './ChatBox';
import { I18nContext } from '../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../i18n/types';
import { translate } from '../i18n/translate';
vi.mock('./chat', () => ({
@@ -113,7 +114,7 @@ vi.mock('./common/KGDropdown', () => ({
default: () => null,
}));
function renderWithLocale(resolvedLocale: 'en_us' | 'zh_cn') {
function renderWithLocale(resolvedLocale: ResolvedLocaleCode) {
return render(
<I18nContext.Provider
value={{
@@ -144,4 +145,10 @@ describe('ChatBox', () => {
expect(screen.getByRole('heading', { level: 3, name: 'K.G.Studio 音乐创作助手' })).toBeTruthy();
});
it('renders the French assistant title under fr_fr', () => {
renderWithLocale('fr_fr');
expect(screen.getByRole('heading', { level: 3, name: 'Assistant musical K.G.Studio' })).toBeTruthy();
});
});
+2 -1
View File
@@ -27,6 +27,7 @@ import {
} from '../test/utils/mock-data';
import { showAlert } from '../util/dialogUtil';
import { I18nContext } from '../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../i18n/types';
import { translate } from '../i18n/translate';
const clickDropdownOption = (label: string) => {
@@ -187,7 +188,7 @@ vi.mock('../core/KGCore', () => ({
}));
describe('EventListPanel', () => {
const renderWithLocale = (locale: 'en_us' | 'zh_cn' = 'en_us') => render(
const renderWithLocale = (locale: ResolvedLocaleCode = 'en_us') => render(
<I18nContext.Provider
value={{
languageSetting: locale,
+2 -1
View File
@@ -4,6 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import InstrumentSelection from './InstrumentSelection';
import { KGMidiTrack } from '../core/track/KGMidiTrack';
import { I18nContext } from '../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../i18n/types';
import { translate } from '../i18n/translate';
const midiTrack = new KGMidiTrack('Lead Track', 1, 'acoustic_grand_piano');
@@ -19,7 +20,7 @@ vi.mock('../stores/projectStore', () => ({
useProjectStore: () => storeState,
}));
function renderWithLocale(resolvedLocale: 'en_us' | 'zh_cn') {
function renderWithLocale(resolvedLocale: ResolvedLocaleCode) {
return render(
<I18nContext.Provider
value={{
+2 -1
View File
@@ -4,6 +4,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import PianoKeys from './PianoKeys';
import { createMockMidiNote, createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
import { I18nContext } from '../../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../../i18n/types';
import { translate } from '../../i18n/translate';
type TestLiveNoteActivityListener = (...args: [{ pitch: number; isNoteOn: boolean }]) => void;
@@ -52,7 +53,7 @@ vi.mock('../../core/midi-input/KGMidiInput', () => ({
function renderWithLocale(
ui: React.ReactElement,
resolvedLocale: 'en_us' | 'zh_cn' = 'en_us',
resolvedLocale: ResolvedLocaleCode = 'en_us',
) {
return render(
<I18nContext.Provider
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { KGPianoRollState, PIANO_ROLL_NO_SNAP } from '../../core/state/KGPianoRollState';
import { createMockMidiControllerEvent, createMockMidiPitchBend, createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
import { I18nContext } from '../../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../../i18n/types';
import { translate } from '../../i18n/translate';
const coreMock = {
@@ -47,7 +48,7 @@ import PianoRollAutomationLane from './PianoRollAutomationLane';
import { getControllerNumberForAutomationType } from './pianoRollAutomation';
describe('PianoRollAutomationLane', () => {
const renderWithLocale = (ui: React.ReactElement, locale: 'en_us' | 'zh_cn' = 'en_us') => render(
const renderWithLocale = (ui: React.ReactElement, locale: ResolvedLocaleCode = 'en_us') => render(
<I18nContext.Provider
value={{
languageSetting: locale,
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import PianoRollToolbar from './PianoRollToolbar';
import { I18nContext } from '../../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../../i18n/types';
import { translate } from '../../i18n/translate';
vi.mock('../common', () => ({
@@ -42,7 +43,7 @@ vi.mock('../../core/KGCore', () => ({
}));
describe('PianoRollToolbar', () => {
const renderWithLocale = (ui: React.ReactElement, locale: 'en_us' | 'zh_cn' = 'en_us') => render(
const renderWithLocale = (ui: React.ReactElement, locale: ResolvedLocaleCode = 'en_us') => render(
<I18nContext.Provider
value={{
languageSetting: locale,
@@ -89,15 +89,15 @@ vi.mock('../../../util/local-separator/modelCache', () => ({
}));
describe('GeneralSettings', () => {
const renderSettings = () => render(
const renderSettings = (locale: 'en_us' | 'zh_cn' = 'en_us') => render(
<I18nContext.Provider
value={{
languageSetting: 'auto',
resolvedLocale: 'en_us',
resolvedLocale: locale,
setLanguageSetting: async (value) => {
await configManagerMock.set('general.language', value);
},
t: (key, params) => translate(key, params, 'en_us'),
t: (key, params) => translate(key, params, locale),
}}
>
<GeneralSettings />
@@ -137,6 +137,24 @@ describe('GeneralSettings', () => {
expect(screen.getByText(/require more VRAM/i)).toBeTruthy();
});
it('uses native language names in the language dropdown', async () => {
renderSettings();
const select = await screen.findByLabelText('Language');
const options = Array.from((select as HTMLSelectElement).options).map((option) => option.text);
expect(options).toEqual(['Auto', 'English', 'Français', '简体中文', '繁體中文']);
});
it('localizes the auto label while keeping language names native', async () => {
renderSettings('zh_cn');
const select = await screen.findByLabelText('语言');
const options = Array.from((select as HTMLSelectElement).options).map((option) => option.text);
expect(options).toEqual(['自动', 'English', 'Français', '简体中文', '繁體中文']);
});
it('initializes the local context length from config', async () => {
renderSettings();
@@ -251,11 +269,12 @@ describe('GeneralSettings', () => {
const languageSelect = await screen.findByLabelText('Language');
expect(languageSelect).toBeTruthy();
expect(screen.getAllByRole('combobox')[0]).toBe(languageSelect);
expect(screen.getByRole('option', { name: 'Français' })).toBeTruthy();
fireEvent.change(languageSelect, { target: { value: 'zh_cn' } });
fireEvent.change(languageSelect, { target: { value: 'fr_fr' } });
await waitFor(() => {
expect(configManagerMock.set).toHaveBeenCalledWith('general.language', 'zh_cn');
expect(configManagerMock.set).toHaveBeenCalledWith('general.language', 'fr_fr');
});
});
});
@@ -19,6 +19,13 @@ import {
LOCAL_SEPARATOR_MODEL_IDS,
} from '../../../util/local-separator/config';
const LANGUAGE_OPTION_LABELS: Record<Exclude<LanguageSetting, 'auto'>, string> = {
en_us: 'English',
fr_fr: 'Français',
zh_cn: '简体中文',
zh_hk: '繁體中文',
};
const GeneralSettings: React.FC = () => {
const { t, setLanguageSetting } = useI18n();
const [language, setLanguage] = useState<LanguageSetting>('auto');
@@ -352,9 +359,10 @@ const GeneralSettings: React.FC = () => {
onChange={(e) => void handleLanguageChange(e.target.value as LanguageSetting)}
>
<option value="auto">{t('settings.general.language.auto')}</option>
<option value="en_us">{t('settings.general.language.en_us')}</option>
<option value="zh_cn">{t('settings.general.language.zh_cn')}</option>
<option value="zh_hk">{t('settings.general.language.zh_hk')}</option>
<option value="en_us">{LANGUAGE_OPTION_LABELS.en_us}</option>
<option value="fr_fr">{LANGUAGE_OPTION_LABELS.fr_fr}</option>
<option value="zh_cn">{LANGUAGE_OPTION_LABELS.zh_cn}</option>
<option value="zh_hk">{LANGUAGE_OPTION_LABELS.zh_hk}</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{t('settings.general.language.help')}
+19
View File
@@ -85,4 +85,23 @@ describe('I18nProvider', () => {
expect(screen.getByText('Settings')).toBeTruthy();
});
});
it('resolves auto to French for browser French locales', async () => {
configState.set('general.language', 'auto');
Object.defineProperty(window.navigator, 'languages', {
configurable: true,
value: ['fr-FR'],
});
render(
<I18nProvider>
<TestComponent />
</I18nProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('locale').textContent).toBe('fr_fr');
expect(screen.getByText('Réglages')).toBeTruthy();
});
});
});
+2
View File
@@ -5,11 +5,13 @@ 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, 'fr_fr'))).toBe('Piano à queue acoustique');
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, 'fr_fr'))).toBe('Pianos et claviers');
expect(getInstrumentGroupLabel('PIANO_AND_KEYBOARDS', (key, params) => translate(key, params, 'zh_cn'))).toBe('钢琴与键盘');
});
+10 -2
View File
@@ -5,15 +5,22 @@ describe('locale resolution', () => {
it('normalizes supported language settings', () => {
expect(normalizeLanguageSetting('auto')).toBe('auto');
expect(normalizeLanguageSetting('en_us')).toBe('en_us');
expect(normalizeLanguageSetting('fr_fr')).toBe('fr_fr');
expect(normalizeLanguageSetting('zh_cn')).toBe('zh_cn');
expect(normalizeLanguageSetting('zh_hk')).toBe('zh_hk');
expect(normalizeLanguageSetting('unknown')).toBe('auto');
});
it('resolves auto to English for non-Chinese locales', () => {
it('resolves auto to English for non-Chinese and non-French locales', () => {
expect(resolveLanguageSetting('auto', ['en-US'])).toBe('en_us');
});
it('resolves auto to French for French-speaking locales', () => {
expect(resolveLanguageSetting('auto', ['fr'])).toBe('fr_fr');
expect(resolveLanguageSetting('auto', ['fr-FR'])).toBe('fr_fr');
expect(resolveLanguageSetting('auto', ['fr-CA'])).toBe('fr_fr');
});
it('resolves auto to Simplified Chinese for Simplified Chinese locales', () => {
expect(resolveLanguageSetting('auto', ['zh-CN'])).toBe('zh_cn');
expect(resolveLanguageSetting('auto', ['zh-SG'])).toBe('zh_cn');
@@ -32,11 +39,12 @@ describe('locale resolution', () => {
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');
expect(resolveLanguageSetting('auto', ['de-DE'])).toBe('en_us');
});
it('keeps explicit locale selections', () => {
expect(resolveLanguageSetting('en_us', ['zh-CN'])).toBe('en_us');
expect(resolveLanguageSetting('fr_fr', ['en-US'])).toBe('fr_fr');
expect(resolveLanguageSetting('zh_cn', ['en-US'])).toBe('zh_cn');
expect(resolveLanguageSetting('zh_hk', ['en-US'])).toBe('zh_hk');
expect(resolveLanguageSetting('zh_hk', ['zh-CN'])).toBe('zh_hk');
+6 -2
View File
@@ -1,13 +1,14 @@
import type { LanguageSetting, ResolvedLocaleCode } from './types';
const CHINESE_LANGUAGE_PREFIX = 'zh';
const FRENCH_LANGUAGE_PREFIX = 'fr';
const DEFAULT_LOCALE: ResolvedLocaleCode = 'en_us';
// BCP 47 subtags that indicate Traditional Chinese (Hant script or HK/TW/MO regions)
const TRADITIONAL_CHINESE_SUBTAGS = new Set(['hk', 'tw', 'mo', 'hant']);
export function normalizeLanguageSetting(value: unknown): LanguageSetting {
return value === 'zh_cn' || value === 'en_us' || value === 'zh_hk' || value === 'auto' ? value : 'auto';
return value === 'zh_cn' || value === 'en_us' || value === 'zh_hk' || value === 'fr_fr' || value === 'auto' ? value : 'auto';
}
export function getBrowserLocales(): string[] {
@@ -28,7 +29,7 @@ export function resolveLanguageSetting(
setting: LanguageSetting,
browserLocales: string[] = getBrowserLocales(),
): ResolvedLocaleCode {
if (setting === 'en_us' || setting === 'zh_cn' || setting === 'zh_hk') {
if (setting === 'en_us' || setting === 'zh_cn' || setting === 'zh_hk' || setting === 'fr_fr') {
return setting;
}
@@ -42,6 +43,9 @@ export function resolveLanguageSetting(
const isTraditional = subtags.some((tag) => TRADITIONAL_CHINESE_SUBTAGS.has(tag));
return isTraditional ? 'zh_hk' : 'zh_cn';
}
if (normalizedLocale === FRENCH_LANGUAGE_PREFIX || normalizedLocale.startsWith(`${FRENCH_LANGUAGE_PREFIX}-`)) {
return 'fr_fr';
}
}
return DEFAULT_LOCALE;
+3 -2
View File
@@ -17,8 +17,9 @@ export const enUsMessages: TranslationMessages = {
'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.general.language.zh_hk': 'Traditional Chinese',
'settings.general.language.fr_fr': 'Français',
'settings.general.language.zh_cn': '简体中文',
'settings.general.language.zh_hk': '繁體中文',
'settings.yes': 'Yes',
'settings.no': 'No',
'settings.restoreDefault': 'Restore default',
+484
View File
@@ -0,0 +1,484 @@
import type { TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
export const frFrMessages: TranslationMessages = {
...enUsMessages,
'app.loading': 'Chargement...',
'assistant.displayName': 'Assistant musical K.G.Studio',
'assistant.welcomeFallback': 'Bienvenue dans lassistant musical K.G.Studio.',
'status.chordGuideCandidate': 'Suggestion du guide daccords : {name} - {notes} - {note}',
'settings.sidebar.title': 'Réglages',
'settings.sidebar.close': 'Fermer les réglages',
'settings.sidebar.general': 'Général',
'settings.sidebar.audioIo': 'Entrées/Sorties audio',
'settings.sidebar.behavior': 'Comportement',
'settings.sidebar.templates': 'Modèles',
'settings.sidebar.chordGuide': 'Guide daccords',
'settings.general.title': 'Général',
'settings.general.language.label': 'Langue',
'settings.general.language.help': 'Le mode Auto utilise la langue du système.',
'settings.general.language.auto': 'Auto',
'settings.yes': 'Oui',
'settings.no': 'Non',
'settings.restoreDefault': 'Rétablir la valeur par défaut',
'settings.deleteCachedModel': 'Supprimer le modèle en cache',
'settings.deleting': 'Suppression...',
'settings.general.llmProvider.section': 'Fournisseur LLM',
'settings.general.llmProvider.label': 'Fournisseur LLM',
'settings.general.llmProvider.local': 'LLM local (navigateur)',
'settings.general.llmProvider.claudeOpenRouter': 'Claude (via OpenRouter)',
'settings.general.llmProvider.openaiCompatible': 'Serveur compatible OpenAI (ex. OpenRouter, Ollama)',
'settings.general.persistKeys.label': 'Conserver les clés API hors localhost',
'settings.general.persistKeys.help': 'Si cette option est activée, les clés API sont enregistrées dans le stockage du navigateur même hors localhost. Attention : cela peut augmenter le risque lié aux attaques XSS.',
'settings.general.localRuntime.cachedStatus': 'État du modèle en cache',
'settings.general.localRuntime.cacheChecking': 'Vérification du cache du modèle local...',
'settings.general.localRuntime.cacheDownloaded': 'Téléchargé dans le cache du navigateur.',
'settings.general.localRuntime.cacheMissing': 'Pas encore téléchargé.',
'settings.general.localRuntime.contextLength': 'Longueur de contexte',
'settings.general.localRuntime.contextHelp': 'Des contextes plus longs demandent davantage de VRAM et peuvent aussi réduire les performances lorsque la conversation devient longue.',
'settings.general.localRuntime.downloadUrl': 'URL de téléchargement',
'settings.general.localRuntime.downloadHelp': 'Modifier cette URL peut empêcher le téléchargement ou pointer vers un fichier modèle incompatible.',
'settings.general.localRuntime.autoDownload': 'Le modèle local sera téléchargé automatiquement lors de votre prochain chat avec `LLM local (navigateur)`.',
'settings.general.uvr5.section': 'Runtime Web UVR5',
'settings.general.uvr5.downloadUrl': 'URL de téléchargement UVR-MDX-NET-Inst_HQ_3',
'settings.general.htdemucs.downloadUrl': 'URL de téléchargement htdemucs_4s',
'settings.general.modelUrl.help': 'Modifier cette URL peut empêcher le téléchargement ou pointer vers un fichier modèle incompatible.',
'settings.general.openai.key': 'Clé',
'settings.general.openai.keyPlaceholder': 'Saisissez votre clé API OpenAI',
'settings.general.openai.model': 'Modèle',
'settings.general.openai.flexMode': 'Mode Flex',
'settings.general.openai.flexHelp': 'Le mode Flex utilise le palier de service flexible dOpenAI. Avantages : économies possibles et meilleur débit pendant les périodes chargées. Inconvénients : latence variable, mise en file dattente ou baisse de priorité possibles. Sapplique uniquement au fournisseur OpenAI ; sans effet pour les serveurs compatibles OpenAI.',
'settings.general.keys.persisted': 'Les clés sont conservées localement (IndexedDB du navigateur).',
'settings.general.keys.sessionOnly': 'Par sécurité, les clés ne sont pas conservées sur les hôtes non locaux et restent uniquement en mémoire pour cette session.',
'settings.general.claudeOpenRouter.section': 'Anthropic Claude (via OpenRouter)',
'settings.general.claudeOpenRouter.keyPlaceholder': 'Saisissez votre clé API Claude',
'settings.general.baseUrl': 'URL de base',
'settings.general.claudeOpenRouter.baseUrlHelp': 'Il sagit de lURL de base de lAPI OpenRouter. Ne la modifiez pas sauf si vous savez précisément ce que vous faites.',
'settings.general.openaiCompatible.section': 'Serveur compatible OpenAI',
'settings.general.openaiCompatible.keyPlaceholder': 'Saisissez votre clé API',
'settings.general.openaiCompatible.baseUrlPlaceholder': 'ex. https://openrouter.ai/api/v1',
'settings.general.openaiCompatible.baseUrlHelp': 'Préréglages rapides :',
'settings.general.openaiCompatible.modelPlaceholder': 'ex. qwen3:30b',
'settings.general.soundfont.section': 'Réglages des soundfonts',
'settings.general.soundfont.managed': 'La configuration des soundfonts est gérée par le serveur (kgone-server.json). Ces réglages sont en lecture seule.',
'settings.general.soundfont.baseUrl': 'URL de base',
'settings.general.soundfont.baseUrlHelp': 'Une source de soundfont incompatible peut provoquer des timbres incorrects ou empêcher certains instruments de jouer.',
'settings.general.kgone.section': 'Réglages K.G.One',
'settings.general.kgone.managed': 'La configuration K.G.One est gérée par le serveur (kgone-server.json). Ces réglages sont en lecture seule.',
'settings.general.kgone.enabled': 'Activer lintégration K.G.One',
'settings.general.kgone.disabled': 'Désactivé',
'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.',
'dialog.title.notice': 'Information',
'dialog.title.timeSignature': 'Chiffrage de mesure',
'dialog.title.tempoDetection': 'Détection du tempo',
'dialog.title.applyTempo': 'Appliquer le tempo',
'dialog.title.chordDetection': 'Détection daccords',
'dialog.title.input': 'Saisie',
'dialog.title.confirm': 'Confirmation',
'dialog.close': 'Fermer la boîte de dialogue',
'dialog.cancel': 'Annuler',
'dialog.experimentalFeature': 'Fonction expérimentale',
'dialog.recommendedSource': 'Source recommandée',
'dialog.chordHint.audio': 'Lanalyse daccords reste expérimentale. Le contenu harmonique, la densité de larrangement et les matériaux riches en transitoires peuvent affecter la précision. Pour de meilleurs résultats, commencez avec les réglages par défaut puis affinez la sensibilité et la stabilité jusqu’à ce que lharmonie détectée corresponde à la phrase musicale.',
'dialog.chordHint.midi': 'Lanalyse daccords reste expérimentale. La densité des voicings, les chevauchements et les notes dornement peuvent influencer le résultat. Pour des étiquettes daccord plus fiables, partez des réglages par défaut puis ajustez la suppression des notes courtes et le focus harmonique selon le rôle musical du passage.',
'dialog.chordHint.tempo': 'Lanalyse du tempo reste expérimentale. Le rubato, les transitoires clairsemées et les couches de percussions peuvent réduire la précision. Commencez avec la plage de BPM par défaut, puis resserrez la fenêtre danalyse vers la zone de tempo la plus plausible si le premier résultat nest pas convaincant musicalement.',
'dialog.sourceHint.kgone': 'Pour des étiquettes daccord plus fiables, analysez de préférence un stem où les voix et les percussions ont été atténuées ou retirées. Si lintégration du serveur K.G.One Music Studio est disponible, lancez Separator avec le modèle "Vocal, Drums, Bass, Guitar, Piano, and Others" et utilisez de préférence le stem Piano ou Others.',
'dialog.sourceHint.local': 'Pour des étiquettes daccord plus fiables, analysez de préférence un stem où les voix et les percussions ont été atténuées ou retirées. Si vous utilisez le séparateur local, choisissez le modèle "Vocal, Drums, Bass, and Others" et analysez le stem Others.',
'dialog.label.sensitivity': 'Sensibilité',
'dialog.label.stability': 'Stabilité',
'dialog.label.noChordThreshold': 'Seuil sans accord',
'dialog.label.enableSevenths': 'Détail harmonique : activer les accords de septième',
'dialog.label.shortNotes': 'Notes courtes',
'dialog.option.suppressionLow': 'Suppression faible',
'dialog.option.suppressionMedium': 'Suppression moyenne',
'dialog.option.suppressionHigh': 'Suppression forte',
'dialog.label.harmonicFocus': 'Focus harmonique',
'dialog.option.harmonicBalanced': 'Équilibré',
'dialog.option.harmonicSustained': 'Privilégier les notes tenues',
'dialog.label.minimumBpm': 'BPM minimum',
'dialog.label.maximumBpm': 'BPM maximum',
'dialog.label.autoAlignRegionToBeat': 'Aligner automatiquement la région sur le temps',
'dialog.message.chordDetection': 'Ajustez les réglages de détection daccords audio avant le traitement.',
'dialog.message.tempoDetection': 'Ajustez les réglages de détection du tempo audio avant le traitement.',
'dialog.message.tempoApply': 'Tempo détecté : {bpm} BPM. Choisissez comment lappliquer.\n\nMettre à jour le tempo courant modifie le tempo actif à lemplacement de ce clip. Insérer un changement de tempo ajoute une nouvelle région de tempo à la mesure la plus proche avant le début du clip.',
'dialog.action.updateCurrentTempo': 'Mettre à jour le tempo courant',
'dialog.action.insertTempoChange': 'Insérer un changement de tempo',
'eventList.title': 'Liste des événements',
'eventList.scopeTabs': 'Portées de la liste d’événements',
'eventList.scope.region': 'Région',
'eventList.scope.track': 'Piste',
'eventList.scope.global': 'Global',
'eventList.deleteVisibleSelectedRows': 'Supprimer les lignes visibles sélectionnées',
'eventList.table.position': 'Position',
'eventList.table.status': 'État',
'eventList.table.lengthInfo': 'Durée/Infos',
'eventList.region.filters': 'Filtres des événements de région',
'eventList.region.filter.notes': 'Notes',
'eventList.region.filter.pitchBends': 'Pitch bends',
'eventList.region.filter.controller': 'Contrôleurs',
'eventList.region.empty': 'Sélectionnez une région MIDI, ou ouvrez-en une dans le Piano Roll, pour afficher sa liste d’événements.',
'eventList.region.add.label': 'Ajouter',
'eventList.region.add.noteTitle': 'Ajouter une note à la tête de lecture',
'eventList.region.add.pitchBendTitle': 'Ajouter un pitch bend à la tête de lecture',
'eventList.region.add.controllerTitle': 'Ajouter un événement de contrôleur à la tête de lecture',
'eventList.region.addType.note': 'Note',
'eventList.region.addType.pitchBend': 'Pitch bend',
'eventList.region.addType.controller': 'Contrôleur',
'eventList.track.filters': 'Modes de la liste de piste',
'eventList.track.filter.regions': 'Régions',
'eventList.track.filter.volume': 'Volume',
'eventList.track.filter.pan': 'Panoramique',
'eventList.track.empty': 'Sélectionnez une piste pour afficher les régions et lautomation de piste.',
'eventList.track.add.midiRegionTitle': 'Ajouter une région MIDI à la tête de lecture',
'eventList.track.add.volumeTitle': 'Ajouter un point dautomation de volume à la tête de lecture',
'eventList.track.add.panTitle': 'Ajouter un point dautomation de panoramique à la tête de lecture',
'eventList.track.addType.midiRegion': 'Région MIDI',
'eventList.track.status.volume': 'Volume',
'eventList.track.status.pan': 'Panoramique',
'eventList.global.filters': 'Filtres des événements globaux',
'eventList.global.filter.marker': 'Repère',
'eventList.global.filter.tempo': 'Tempo',
'eventList.global.filter.keySignature': 'Armure',
'eventList.global.filter.chord': 'Accord',
'eventList.global.add.markerTitle': 'Ajouter une région de repère à la tête de lecture',
'eventList.global.add.tempoTitle': 'Ajouter une région de tempo à la tête de lecture',
'eventList.global.add.keySignatureTitle': 'Ajouter une région darmure à la tête de lecture',
'eventList.global.add.chordTitle': 'Ajouter une région daccord à la tête de lecture',
'eventList.global.addType.marker': 'Repère',
'eventList.global.addType.tempo': 'Tempo',
'eventList.global.addType.keySignature': 'Armure',
'eventList.global.addType.chord': 'Accord',
'eventList.global.status.marker': 'Repère',
'eventList.global.status.keySignature': 'Armure',
'eventList.global.status.chord': 'Accord',
'eventList.global.validation.marker': 'Veuillez saisir un libellé de repère. Un texte non vide est attendu. Exemple : Intro',
'eventList.global.validation.tempo': 'Veuillez saisir une valeur BPM en chiffres uniquement. Un entier entre {min} et {max} est attendu. Exemple : 128',
'eventList.global.validation.keySignature': 'Veuillez saisir un nom darmure exact. Une valeur canonique telle que "Do majeur" ou "Fa# mineur" est attendue. Exemple : Fa# mineur',
'eventList.global.validation.chord': 'Veuillez saisir un symbole daccord valide. Une écriture daccord que lapplication peut analyser est attendue. Exemple : Bm7b5',
'eventList.global.validation.position': 'Veuillez saisir une position au début du projet ou après. Une position non négative est attendue. Exemple : 1 1 0',
'eventList.global.validation.length': 'Veuillez saisir une durée positive. Une valeur supérieure à zéro est attendue. Exemple : 4 0',
'globalTracks.label': 'Pistes globales',
'globalTracks.marker': 'Repère',
'globalTracks.signature': 'Armure',
'globalTracks.chord': 'Accord',
'globalTracks.addItem': 'Ajouter un élément global {label}',
'pianoRoll.power.sqrtDefault': '√ (par défaut)',
'pianoRoll.chordGuide.off': 'Guide daccords désactivé',
'pianoRoll.chordGuide.tonic': 'Guide daccords tonique',
'pianoRoll.chordGuide.subdominant': 'Guide daccords sous-dominante',
'pianoRoll.chordGuide.dominant': 'Guide daccords dominante',
'pianoRoll.spectrogramView': 'Vue spectrogramme',
'pianoRoll.sheetMusicView': 'Vue partition',
'pianoRoll.pointerTool': 'Outil pointeur',
'pianoRoll.pencilTool': 'Outil crayon',
'pianoRoll.toggleAutomationLane': 'Afficher/masquer la voie dautomation',
'pianoRoll.automation': 'Automation',
'pianoRoll.automationLane': 'Voie dautomation {label}',
'pianoRoll.automationType.pitchBend': 'Pitch bend',
'pianoRoll.chordGuide': 'Guide daccords',
'pianoRoll.sheetQuantization': 'Quantif. partition',
'pianoRoll.snap': 'Snap',
'pianoRoll.snap.none': 'SANS SNAP',
'pianoRoll.quantizePositionCompact': 'Quant. pos.',
'pianoRoll.quantizeLengthCompact': 'Quant. dur.',
'pianoRoll.floor': 'Plancher',
'pianoRoll.curve': 'Courbe',
'pianoRoll.zoom': 'Zoom',
'pianoRoll.moreOptions': 'Plus doptions',
'pianoRoll.showActiveRegionOnly': 'Afficher seulement la région active',
'pianoRoll.showEntireTrack': 'Afficher toute la piste',
'pianoRoll.detectChords': 'Détecter les accords...',
'pianoRoll.detectingChords': 'Détection des accords...',
'pianoRoll.detectTempo': 'Détecter le tempo...',
'pianoRoll.detectingTempo': 'Détection du tempo...',
'toolbar.projectName.prompt': 'Saisissez le nom du projet :',
'toolbar.projectName.invalid': 'Nom de projet invalide. Seules les lettres, les chiffres, les espaces, les tirets, les underscores, les points et les parenthèses sont autorisés.',
'toolbar.projectName.reserved': '"{name}" est un nom de projet réservé. Choisissez-en un autre.',
'toolbar.projectName.overwrite': 'Le projet "{name}" existe déjà. Voulez-vous l’écraser ?',
'toolbar.projectName.renameOrCopy': 'Souhaitez-vous renommer ce projet ou lenregistrer comme nouvelle copie ?',
'toolbar.projectName.saveAsCopy': 'Enregistrer comme copie',
'toolbar.projectName.rename': 'Renommer',
'toolbar.save.error': 'Une erreur est survenue lors de lenregistrement : {error}',
'toolbar.load.error': 'Une erreur est survenue lors du chargement du projet : {error}',
'toolbar.newProject.confirm': 'Créer un nouveau projet ? Toutes les modifications non enregistrées seront perdues.',
'toolbar.project.notFound': 'Projet "{name}" introuvable.',
'toolbar.openProject.confirm': 'Ouvrir ce projet ? Toutes les modifications non enregistrées du projet actuel seront perdues.',
'toolbar.status.loadFailed': 'Échec du chargement du projet : {error}',
'toolbar.status.newProjectCreated': 'Nouveau projet "{name}" créé',
'toolbar.status.projectLoaded': '{description} chargé avec succès',
'toolbar.status.projectExportedKgstudio': 'Projet "{name}" exporté au format KGStudio',
'toolbar.status.exportProjectError': 'Erreur lors de lexport du projet : {error}',
'toolbar.export.failedProject': 'Échec de lexport du projet : {error}',
'toolbar.status.projectExportedMidi': 'Projet "{name}" exporté au format MIDI',
'toolbar.status.exportMidiError': 'Erreur lors de lexport MIDI : {error}',
'toolbar.export.failedMidi': 'Échec de lexport du projet en MIDI : {error}',
'toolbar.status.projectExportedWav': 'Projet "{name}" exporté en WAV',
'toolbar.status.exportWavError': 'Erreur lors de lexport WAV : {error}',
'toolbar.export.failedWav': 'Échec de lexport du projet en WAV : {error}',
'toolbar.status.projectExportedMp3': 'Projet "{name}" exporté en MP3',
'toolbar.status.exportMp3Error': 'Erreur lors de lexport MP3 : {error}',
'toolbar.export.failedMp3': 'Échec de lexport du projet en MP3 : {error}',
'toolbar.status.importFailed': 'Échec de limport du fichier : {error}',
'toolbar.import.failedProjectFile': 'Échec de limport du fichier projet : {error}',
'toolbar.import.corruptedKgstudio': 'Le fichier .kgstudio est corrompu ou invalide : {error}',
'toolbar.status.importingMidi': 'Import du fichier MIDI "{name}"...',
'toolbar.status.failedImportMidi': 'Échec de limport MIDI : {error}',
'toolbar.status.playbackFailedStart': 'Impossible de démarrer la lecture',
'toolbar.status.recordingStoppedCommitted': 'Enregistrement arrêté, notes validées',
'toolbar.status.failedStopPlayback': 'Impossible darrêter la lecture',
'toolbar.maxBars.prompt': 'Saisissez le nouveau nombre maximal de mesures (>= {min}) :',
'toolbar.input.invalidNumber': 'Entrée invalide. Veuillez saisir un nombre valide.',
'toolbar.maxBars.invalid': 'Valeur invalide. Veuillez saisir un nombre >= {min}.',
'toolbar.status.maxBarsChanged': 'Nombre maximal de mesures défini sur {value}',
'toolbar.bpm.prompt': 'Saisissez le nouveau BPM ({min}-{max}) :',
'toolbar.bpm.invalid': 'BPM invalide. Veuillez saisir une valeur entre {min} et {max}.',
'toolbar.status.bpmChanged': 'BPM changé à {value}',
'toolbar.timeSignature.prompt': 'Définir le chiffrage de mesure :',
'toolbar.status.timeSignatureChanged': 'Chiffrage de mesure changé à {value}',
'toolbar.status.keySignatureChanged': 'Armure changée à {value}',
'toolbar.status.copied': 'Éléments copiés dans le presse-papiers',
'toolbar.status.copyNone': 'Aucun élément sélectionné à copier',
'toolbar.status.pasted': 'Éléments collés depuis le presse-papiers',
'toolbar.status.pasteFailed': 'Impossible de coller : contenu ou contexte du presse-papiers invalide',
'toolbar.status.deleted': 'Régions sélectionnées supprimées',
'toolbar.status.deleteNone': 'Aucune région sélectionnée à supprimer',
'toolbar.undo.none': 'Rien à annuler',
'toolbar.redo.none': 'Rien à rétablir',
'toolbar.status.undid': 'Action annulée : {description}',
'toolbar.status.redid': 'Action rétablie : {description}',
'toolbar.status.chatToggled': 'Chat affiché/masqué',
'toolbar.status.settingsToggled': 'Réglages affichés/masqués',
'toolbar.pianoRoll.selectMidiRegion': 'Sélectionnez une région MIDI pour ouvrir le Piano Roll.',
'toolbar.status.recordingStopped': 'Enregistrement arrêté',
'toolbar.status.audioRecordingStarted': 'Enregistrement audio démarré...',
'toolbar.recording.openMidiRegion': 'Ouvrez une région MIDI dans le Piano Roll avant de lancer lenregistrement.',
'toolbar.recording.selectMidiRegion': 'Sélectionnez une région MIDI avant de lancer lenregistrement.',
'toolbar.recording.noMidiDevice': 'Aucun périphérique MIDI détecté. Connectez un clavier MIDI puis réessayez.',
'toolbar.status.recordingStarted': 'Enregistrement démarré...',
'toolbar.button.new': 'Nouveau',
'toolbar.button.load': 'Ouvrir',
'toolbar.button.save': 'Enregistrer',
'toolbar.button.export': 'Exporter',
'toolbar.button.import': 'Importer',
'toolbar.button.settings': 'Réglages',
'toolbar.button.undo': 'Annuler',
'toolbar.button.redo': 'Rétablir',
'toolbar.button.select': 'Sélection',
'toolbar.button.pencil': 'Crayon',
'toolbar.button.splitRegion': 'Couper la région à la tête de lecture',
'toolbar.button.mergeRegions': 'Fusionner les régions MIDI sélectionnées',
'toolbar.button.snapToGrid': 'Aligner à la grille',
'toolbar.button.copy': 'Copier',
'toolbar.button.paste': 'Coller',
'toolbar.button.delete': 'Supprimer',
'toolbar.button.backToBeginning': 'Retour au début',
'toolbar.button.play': 'Lecture',
'toolbar.button.pause': 'Pause',
'toolbar.button.stopRecording': 'Arrêter lenregistrement',
'toolbar.button.record': 'Enregistrer',
'toolbar.button.loop': 'Boucle',
'toolbar.button.metronome': 'Métronome',
'toolbar.button.piano': 'Piano',
'toolbar.button.kgone': 'Générateur musical K.G.One',
'toolbar.button.chat': 'Chat',
'toolbar.button.eventList': 'Éditeur de liste d’événements',
'toolbar.keySignatureChooser': 'Choisir larmure, actuelle : {value}',
'toolbar.export.label': 'Exporter',
'toolbar.importProject.title': 'Importer un projet',
'toolbar.importProject.description': 'Glissez-déposez votre fichier projet ici',
'toolbar.openingProject': 'Ouverture du projet...',
'instrument.group.PIANO_AND_KEYBOARDS': 'Pianos et claviers',
'instrument.group.GUITAR': 'Guitare',
'instrument.group.BASS': 'Basse',
'instrument.group.STRINGS': 'Cordes',
'instrument.group.BRASS': 'Cuivres',
'instrument.group.WOODWIND': 'Bois',
'instrument.group.PERCUSSION_KIT': 'Batterie et percussions',
'instrument.group.SYNTH': 'Synthétiseur',
'instrument.name.acoustic_grand_piano': 'Piano à queue acoustique',
'instrument.name.bright_acoustic_piano': 'Piano acoustique brillant',
'instrument.name.electric_grand_piano': 'Piano à queue électrique',
'instrument.name.electric_piano_1': 'Piano électrique 1',
'instrument.name.electric_piano_2': 'Piano électrique 2',
'instrument.name.drawbar_organ': 'Orgue à tirettes',
'instrument.name.clavinet': 'Clavinet',
'instrument.name.harpsichord': 'Clavecin',
'instrument.name.accordion': 'Accordéon',
'instrument.name.acoustic_guitar_nylon': 'Guitare acoustique (nylon)',
'instrument.name.acoustic_guitar_steel': 'Guitare acoustique (acier)',
'instrument.name.electric_guitar_clean': 'Guitare électrique (clair)',
'instrument.name.overdriven_guitar': 'Guitare électrique (overdrive)',
'instrument.name.distortion_guitar': 'Guitare électrique (distorsion)',
'instrument.name.acoustic_bass': 'Basse acoustique',
'instrument.name.electric_bass_finger': 'Basse électrique (doigts)',
'instrument.name.electric_bass_pick': 'Basse électrique (médiator)',
'instrument.name.slap_bass_1': 'Basse slap',
'instrument.name.synth_bass_1': 'Basse synthé 1',
'instrument.name.violin': 'Violon',
'instrument.name.viola': 'Alto',
'instrument.name.cello': 'Violoncelle',
'instrument.name.contrabass': 'Contrebasse',
'instrument.name.string_ensemble_1': 'Ensemble de cordes',
'instrument.name.string_ensemble_2': 'Ensemble de cordes 2',
'instrument.name.pizzicato_strings': 'Cordes pizzicato',
'instrument.name.tremolo_strings': 'Cordes trémolo',
'instrument.name.orchestral_harp': 'Harpe orchestrale',
'instrument.name.trumpet': 'Trompette',
'instrument.name.trombone': 'Trombone',
'instrument.name.tuba': 'Tuba',
'instrument.name.french_horn': 'Cor français',
'instrument.name.brass_section': 'Section de cuivres',
'instrument.name.soprano_sax': 'Saxophone soprano',
'instrument.name.alto_sax': 'Saxophone alto',
'instrument.name.tenor_sax': 'Saxophone ténor',
'instrument.name.baritone_sax': 'Saxophone baryton',
'instrument.name.oboe': 'Hautbois',
'instrument.name.english_horn': 'Cor anglais',
'instrument.name.bassoon': 'Basson',
'instrument.name.clarinet': 'Clarinette',
'instrument.name.piccolo': 'Piccolo',
'instrument.name.flute': 'Flûte',
'instrument.name.shakuhachi': 'Shakuhachi',
'instrument.name.harmonica': 'Harmonica',
'instrument.name.standard': 'Kit de batterie standard',
'instrument.name.orchestra_kit': 'Kit de percussions orchestrales',
'instrument.name.marimba': 'Marimba',
'instrument.name.timpani': 'Timbales',
'instrument.name.taiko_drum': 'Tambour taiko',
'instrument.name.woodblock': 'Wood-block',
'instrument.name.lead_1_square': 'Lead 1 (carré)',
'instrument.name.lead_2_sawtooth': 'Lead 2 (dent de scie)',
'instrument.name.pad_1_new_age': 'Pad 1 (new age)',
'instrument.name.pad_2_warm': 'Pad 2 (chaud)',
'instrument.name.pad_3_polysynth': 'Pad 3 (polysynth)',
'instrument.name.pad_4_choir': 'Pad 4 (chœur)',
'instrument.name.pad_5_bowed': 'Pad 5 (verre frotté)',
'instrument.name.pad_6_metallic': 'Pad 6 (métallique)',
'instrument.name.pad_7_halo': 'Pad 7 (halo)',
'instrument.name.pad_8_sweep': 'Pad 8 (balayage)',
'instrument.name.fx_1_rain': 'FX 1 (pluie)',
'instrument.name.fx_2_soundtrack': 'FX 2 (bande-son)',
'instrument.name.fx_3_crystal': 'FX 3 (cristal)',
'instrument.name.fx_4_atmosphere': 'FX 4 (atmosphère)',
'instrument.name.fx_5_brightness': 'FX 5 (brillance)',
'instrument.name.fx_6_goblins': 'FX 6 (gobelins)',
'instrument.name.fx_7_echoes': 'FX 7 (échos)',
'instrument.name.fx_8_scifi': 'FX 8 (science-fiction)',
'kgone.panel.title.server': 'Générateur musical K.G.One',
'kgone.panel.title.local': 'Générateur musical',
'kgone.tab.fullSong': 'Morceau complet',
'kgone.tab.separator': 'Séparateur',
'kgone.tab.requiresServer': 'Nécessite lintégration du serveur K.G.One Music Studio',
'kgone.shared.advancedSettings': 'Réglages avancés',
'kgone.shared.caption': 'Description',
'kgone.shared.lyrics': 'Paroles',
'kgone.shared.instrumental': 'Instrumental (sans voix)',
'kgone.shared.inferenceSteps': 'Étapes dinférence',
'kgone.shared.guidanceScale': 'Échelle de guidage',
'kgone.shared.useRandomSeed': 'Utiliser une seed aléatoire',
'kgone.shared.seed': 'Seed',
'kgone.shared.thinking': 'Réflexion (génération de métadonnées CoT)',
'kgone.shared.selectedRegion': 'Région sélectionnée',
'kgone.shared.track': 'Piste',
'kgone.shared.btn.loadingModel': 'Chargement du modèle...',
'kgone.shared.btn.generating': 'Génération...',
'kgone.shared.btn.processing': 'Traitement...',
'kgone.shared.btn.downloading': 'Téléchargement...',
'kgone.shared.btn.preparingUpload': 'Préparation de lenvoi...',
'kgone.shared.btn.importing': 'Import...',
'kgone.shared.hint.loadingModel': 'Chargement du modèle : cela peut prendre plus de 60 secondes, veuillez patienter...',
'kgone.shared.hint.submitting': 'Envoi de la demande de génération...',
'kgone.shared.hint.downloadingAudio': 'Téléchargement de laudio...',
'kgone.clip.field.prompt': 'Prompt',
'kgone.clip.field.promptHint': 'Décrivez le clip avec des tags séparés par des virgules : famille dinstrument, sous-type, timbre, effets, nombre de mesures, BPM, tonalité.',
'kgone.clip.field.negativePrompt': 'Prompt négatif',
'kgone.clip.field.bars': 'Mesures',
'kgone.clip.field.note': 'Note',
'kgone.clip.field.scale': 'Mode',
'kgone.clip.field.scaleMajor': 'Majeur',
'kgone.clip.field.scaleMinor': 'Mineur',
'kgone.clip.field.steps': 'Étapes',
'kgone.clip.field.seedLabel': 'Seed (-1 = aléatoire)',
'kgone.clip.field.samplerType': 'Type de sampler',
'kgone.clip.hint.generating': 'Génération du clip...',
'kgone.clip.hint.drag': 'Glissez le lecteur ci-dessus vers une piste pour importer le clip. Déposez-le sur une <strong>piste audio</strong> pour limporter comme région WAV (recommandé), ou sur une <strong>piste MIDI</strong> pour limporter comme région MIDI. Remarque : le MIDI est retranscrit depuis laudio et peut ne pas être parfaitement exact.',
'kgone.clip.btn.generate': 'Générer le clip',
'kgone.fullSong.field.captionHint': 'Décrivez en langage naturel le style, lambiance, le tempo, linstrumentation et la structure du morceau.',
'kgone.fullSong.field.lyricsHint': 'Utilisez les balises [Intro], [Verse], [Chorus], [Bridge] pour marquer les sections.',
'kgone.fullSong.hint.generating': 'Génération...',
'kgone.fullSong.hint.generatingProgress': 'Génération... {pct}% — {stage}',
'kgone.fullSong.hint.generatingProgressNoStage': 'Génération... {pct}%',
'kgone.fullSong.hint.drag': 'Glissez le lecteur ci-dessus vers une <strong>piste audio</strong> pour importer le morceau. Le dépôt sur une piste MIDI nest pas pris en charge pour la génération de morceau complet.',
'kgone.fullSong.btn.generate': 'Générer le morceau',
'kgone.remix.field.captionHint': 'Décrivez le style, lambiance et linstrumentation visés pour le remix.',
'kgone.remix.field.lyricsHint': 'Laissez vide pour conserver les paroles dorigine, ou saisissez-en de nouvelles. Utilisez les balises [Verse], [Chorus], [Bridge].',
'kgone.remix.field.coverStrength': 'Intensité de reprise',
'kgone.remix.field.coverStrengthHint': '0 = créatif, 1 = fidèle à la structure source',
'kgone.remix.field.noiseStrength': 'Intensité du bruit',
'kgone.remix.field.noiseStrengthHint': '0 = transfert de style pur, 0,1 à 0,25 recommandé',
'kgone.remix.hint.submitting': 'Envoi de la demande de remix...',
'kgone.remix.hint.generating': 'Génération du remix...',
'kgone.remix.hint.generatingProgress': 'Génération du remix... {pct}% — {stage}',
'kgone.remix.hint.generatingProgressNoStage': 'Génération du remix... {pct}%',
'kgone.remix.hint.drag': 'Glissez le lecteur ci-dessus vers une <strong>piste audio</strong> pour importer le remix.',
'kgone.remix.hint.noRegion': 'Sélectionnez une région audio sur la timeline pour la remixer. Seules les régions audio sont prises en charge ; les régions MIDI ne peuvent pas être remixées.',
'kgone.remix.btn.generate': 'Générer le remix',
'kgone.remix.btn.processingRemix': 'Traitement du remix...',
'kgone.remix.btn.importAligned': 'Importer aligné sur la source',
'kgone.repaint.field.repaintRange': 'Plage de repaint',
'kgone.repaint.field.loopModeOff': 'Le mode boucle est désactivé. Activez le bouton boucle dans la barre doutils et définissez une plage de boucle pour fixer la fenêtre de repaint.',
'kgone.repaint.field.start': 'Début',
'kgone.repaint.field.end': 'Fin',
'kgone.repaint.field.untilEnd': 'jusqu’à la fin de laudio',
'kgone.repaint.field.captionHint': 'Décrivez le style cible et linstrumentation de la section repeinte.',
'kgone.repaint.field.lyricsHint': 'Laissez vide pour conserver les paroles dorigine, ou saisissez-en de nouvelles pour la section repeinte.',
'kgone.repaint.field.repaintStrength': 'Intensité du repaint',
'kgone.repaint.field.repaintStrengthHint': '0 = préserver loriginal, 1 = régénération complète',
'kgone.repaint.hint.submitting': 'Envoi de la demande de repaint...',
'kgone.repaint.hint.generating': 'Génération du repaint...',
'kgone.repaint.hint.generatingProgress': 'Génération du repaint... {pct}% — {stage}',
'kgone.repaint.hint.generatingProgressNoStage': 'Génération du repaint... {pct}%',
'kgone.repaint.hint.drag': 'Glissez le lecteur ci-dessus vers une <strong>piste audio</strong> pour importer le repaint.',
'kgone.repaint.hint.noRegion': 'Sélectionnez une région audio sur la timeline pour la repeindre. Seules les régions audio sont prises en charge ; les régions MIDI ne peuvent pas être repeintes.',
'kgone.repaint.btn.generate': 'Générer le repaint',
'kgone.repaint.btn.processingRepaint': 'Traitement du repaint...',
'kgone.repaint.btn.importAligned': 'Importer aligné sur la source',
'kgone.separator.field.separationModel': 'Modèle de séparation',
'kgone.separator.field.chunkDuration': 'Durée optionnelle des segments audio (secondes)',
'kgone.separator.field.chunkDurationPlaceholder': 'Laisser vide pour traiter toute la région',
'kgone.separator.field.modelOverlap': 'Recouvrement du modèle',
'kgone.separator.hint.drag': 'Glissez chaque lecteur de stem ci-dessus vers une <strong>piste audio</strong> pour limporter. Le dépôt sur une piste MIDI nest pas pris en charge pour la séparation de stems.',
'kgone.separator.hint.noRegion.download': 'Téléchargez {model}, puis sélectionnez une région audio sur la timeline pour en extraire les stems.',
'kgone.separator.hint.noRegion.select': 'Sélectionnez une région audio sur la timeline pour en extraire les stems. Seules les régions audio sont prises en charge ; les régions MIDI ne peuvent pas être séparées.',
'kgone.separator.btn.separateStems': 'Séparer les stems',
'kgone.separator.btn.importAllStems': 'Importer tous les stems sur la timeline',
'kgone.separator.btn.server.polling': 'Séparation des stems...',
'kgone.separator.btn.local.loadingModel': 'Préparation du modèle local...',
'kgone.separator.btn.local.generating': 'Préparation de laudio...',
'kgone.separator.btn.local.polling': 'Séparation locale en cours...',
'kgone.separator.btn.local.downloading': 'Finalisation...',
'kgone.separator.local.title': 'Mode séparateur local',
'kgone.separator.local.description': 'Si K.G.One Music Studio nest pas disponible, le mode séparateur local offre une solution intégrée pour extraire des stems directement dans votre navigateur. Deux modèles locaux sont disponibles : Vocal and Instrument (Medium Accuracy), et Vocal, Drums, Bass, and Others. L’état de téléchargement ci-dessous correspond au modèle actuellement sélectionné. Vocal and Instrument (Medium Accuracy) prend généralement plus de temps à traiter que Vocal, Drums, Bass, and Others, et la durée totale dépendra tout de même de votre matériel. Si le traitement bascule sur le CPU, la page peut devenir temporairement moins réactive pendant la séparation.',
'kgone.separator.local.learnMore': 'En savoir plus sur lintégration du serveur K.G.One Music Studio.',
'kgone.separator.local.provider': 'Fournisseur : ',
'kgone.separator.local.model': 'Modèle : ',
'kgone.separator.local.downloaded': 'téléchargé',
'kgone.separator.local.notDownloaded': 'non téléchargé',
'kgone.separator.local.checkingCache': 'Vérification du cache du modèle local...',
'kgone.separator.local.btn.download': 'Télécharger le modèle sélectionné',
'kgone.separator.local.btn.downloading': 'Téléchargement du modèle...',
'kgone.separator.local.btn.redownload': 'Retélécharger le modèle',
'kgone.separator.local.btn.redownloading': 'Retéléchargement...',
'kgone.separator.local.btn.deleteCache': 'Supprimer le modèle en cache',
'kgone.separator.local.btn.deleting': 'Suppression...',
'kgone.separator.local.progress.downloading': 'Téléchargement de {name}...',
'kgone.separator.local.progress.downloadingWithSize': 'Téléchargement de {name}... {received} / {total} MB',
'kgone.separator.local.progress.downloadingMbOnly': 'Téléchargement de {name}... {received} MB',
'kgone.separator.local.progress.ready': '{name} est prêt.',
'kgone.separator.local.progress.preparingRuntime': 'Préparation de la session ONNX Runtime...',
'kgone.separator.local.progress.readingAudio': 'Lecture du fichier audio...',
'kgone.separator.local.progress.running': 'Séparation dans le navigateur en cours...',
'kgone.separator.local.progress.complete': 'Séparation terminée.',
};
+2
View File
@@ -1,10 +1,12 @@
import type { ResolvedLocaleCode, TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
import { frFrMessages } from './fr_fr';
import { zhCnMessages } from './zh_cn';
import { zhHkMessages } from './zh_hk';
export const messagesByLocale: Record<ResolvedLocaleCode, TranslationMessages> = {
en_us: enUsMessages,
fr_fr: frFrMessages,
zh_cn: zhCnMessages,
zh_hk: zhHkMessages,
};
+2 -3
View File
@@ -1,6 +1,8 @@
import type { TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
export const zhCnMessages: TranslationMessages = {
...enUsMessages,
'app.loading': '加载中...',
'assistant.displayName': 'K.G.Studio 音乐创作助手',
'assistant.welcomeFallback': '欢迎使用 K.G.Studio 音乐创作助手。',
@@ -16,9 +18,6 @@ export const zhCnMessages: TranslationMessages = {
'settings.general.language.label': '语言',
'settings.general.language.help': '自动会使用你的系统语言。',
'settings.general.language.auto': '自动',
'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': '简体中文',
'settings.general.language.zh_hk': '繁体中文',
'settings.yes': '是',
'settings.no': '否',
'settings.restoreDefault': '恢复默认',
+2 -3
View File
@@ -1,6 +1,8 @@
import type { TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
export const zhHkMessages: TranslationMessages = {
...enUsMessages,
'app.loading': '加載中...',
'assistant.displayName': 'K.G.Studio 音樂創作助手',
'assistant.welcomeFallback': '歡迎使用 K.G.Studio 音樂創作助手。',
@@ -16,9 +18,6 @@ export const zhHkMessages: TranslationMessages = {
'settings.general.language.label': '語言',
'settings.general.language.help': '自動會使用你的系統語言。',
'settings.general.language.auto': '自動',
'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': '簡體中文',
'settings.general.language.zh_hk': '繁體中文',
'settings.yes': '是',
'settings.no': '否',
'settings.restoreDefault': '恢復默認',
+5 -2
View File
@@ -2,18 +2,21 @@ import { describe, expect, it } from 'vitest';
import { translate } from './translate';
describe('translate', () => {
it('returns translated strings for both locales', () => {
it('returns translated strings for supported locales', () => {
expect(translate('settings.general.language.auto', undefined, 'en_us')).toBe('Auto');
expect(translate('settings.general.language.auto', undefined, 'fr_fr')).toBe('Auto');
expect(translate('settings.general.language.auto', undefined, 'zh_cn')).toBe('自动');
});
it('falls back to English when a zh-CN key is missing', () => {
it('falls back to English when a locale key is missing', () => {
expect(translate('toolbar.button.new', undefined, 'zh_cn')).toBe('新建');
expect(translate('nonexistent.key', undefined, 'fr_fr')).toBe('nonexistent.key');
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 }, 'fr_fr')).toBe('BPM changé à 120');
expect(translate('toolbar.status.bpmChanged', { value: 120 }, 'zh_cn')).toBe('BPM 已改为 120');
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
export type LanguageSetting = 'auto' | 'en_us' | 'zh_cn' | 'zh_hk';
export type LanguageSetting = 'auto' | 'en_us' | 'zh_cn' | 'zh_hk' | 'fr_fr';
export type ResolvedLocaleCode = Exclude<LanguageSetting, 'auto'>;
@@ -111,6 +111,17 @@ describe('processUserMessage slash commands', () => {
expect(result.pseudoAssistantResponse).toContain('welcome_local_llm-zh_cn.md');
});
it('uses the localized welcome asset when fr-FR is selected', async () => {
configState.set('general.language', 'fr_fr');
configState.set('general.llm_provider', 'local_browser');
const result = await processUserMessage('/welcome');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_local_llm-fr_fr.md'));
expect(result.metadata).toMatchObject({ command: 'welcome', variant: 'local' });
expect(result.pseudoAssistantResponse).toContain('welcome_local_llm-fr_fr.md');
});
it('falls back to a localized welcome string when welcome markdown fetch fails', async () => {
configState.set('general.language', 'zh_cn');
configState.set('general.llm_provider', 'local_browser');
@@ -170,6 +181,19 @@ describe('processUserMessage slash commands', () => {
expect(message?.content).toContain('welcome_local_llm-zh_cn.md');
});
it('uses localized welcome assets for addWelcomeMessage under auto + French locale', async () => {
configState.set('general.language', 'auto');
vi.stubGlobal('navigator', {
languages: ['fr-FR', 'en-US'],
language: 'fr-FR',
});
const message = await addWelcomeMessage();
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/welcome_local_llm-fr_fr.md'));
expect(message?.content).toContain('welcome_local_llm-fr_fr.md');
});
it('fetches the localized help guide for /help under zh-CN', async () => {
configState.set('general.language', 'zh_cn');
@@ -185,6 +209,15 @@ describe('processUserMessage slash commands', () => {
expect(result.pseudoAssistantResponse).toContain('chat/help-zh_cn.md');
});
it('fetches the localized help guide for /help under fr-FR', async () => {
configState.set('general.language', 'fr_fr');
const result = await processUserMessage('/help');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/help-fr_fr.md'));
expect(result.pseudoAssistantResponse).toContain('chat/help-fr_fr.md');
});
it('falls back to the English help guide when the localized file is missing', async () => {
configState.set('general.language', 'zh_cn');
vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request) => {
@@ -223,6 +256,15 @@ describe('processUserMessage slash commands', () => {
expect(result.pseudoAssistantResponse).toContain('chat/hotkeys.md');
});
it('fetches the localized hotkeys guide for /hotkeys under fr-FR', async () => {
configState.set('general.language', 'fr_fr');
const result = await processUserMessage('/hotkeys');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('chat/hotkeys-fr_fr.md'));
expect(result.pseudoAssistantResponse).toContain('chat/hotkeys-fr_fr.md');
});
it('fetches the localized hotkeys guide for /hotkeys under zh-CN', async () => {
configState.set('general.language', 'zh_cn');