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
+107
View File
@@ -0,0 +1,107 @@
## Aide
Bienvenue dans **lassistant musical K.G.Studio**, votre agent IA dédié à la composition, à larrangement et au travail de production directement dans le navigateur.
Pour commencer, vous pouvez soit utiliser le **LLM local intégré au navigateur** sans clé API, soit configurer un fournisseur LLM externe.
---
### Utiliser le LLM local (navigateur) sans clé API
K.G.Studio peut exécuter **Gemma 4 E4B** entièrement dans votre navigateur avec accélération WebGPU. Aucune requête API externe nest envoyée, aucun coût supplémentaire nest engagé, et vos données restent sur votre machine.
1. Dans **Réglages ⚙️ → Général → Fournisseur LLM**, sélectionnez **LLM local (navigateur)**.
2. Le modèle (~2,8 Go) se télécharge automatiquement lors de la première ouverture du chat, puis reste en cache local pour les lancements suivants.
3. Vous pouvez régler la **Longueur de contexte** (32k / 64k / 128k tokens) selon votre matériel.
4. Commencez à dialoguer.
**Configuration recommandée :** Chrome 113+ ou Edge 113+, contexte sécurisé (HTTPS ou localhost), et GPU avec au moins 8 Go de VRAM ou machine avec au moins 16 Go de mémoire unifiée.
> Remarque : la qualité du modèle local reste inférieure à celle des grands modèles commerciaux. Pour les tâches complexes, un fournisseur externe donnera généralement de meilleurs résultats.
---
### Configurer un fournisseur LLM externe
Allez dans **Réglages ⚙️ → Général → Fournisseur LLM**. Selon le fournisseur choisi, vous devrez renseigner la clé API appropriée et, si besoin, une URL de base personnalisée.
---
### Utiliser OpenAI
1. Récupérez une clé API OpenAI depuis [**OpenAI**](https://platform.openai.com/account/api-keys).
2. Dans **Réglages ⚙️ → Général → Fournisseur LLM**, choisissez **OpenAI**.
3. Saisissez votre clé dans **OpenAI → Clé**.
4. Sélectionnez le modèle voulu dans **OpenAI → Modèle**. Pour un bon compromis coût / performances, `gpt-5.4-mini` est recommandé.
5. Vous pouvez activer **Mode Flex** si vous acceptez une latence plus variable en échange dun coût potentiellement réduit.
---
### Utiliser OpenRouter
OpenRouter donne accès à de nombreux modèles via une API unique, y compris certaines options gratuites.
1. Créez une clé API sur [**OpenRouter**](https://openrouter.ai/keys).
2. Dans **Réglages ⚙️ → Général → Fournisseur LLM**, choisissez **Serveur compatible OpenAI**.
3. Saisissez votre clé dans **Serveur compatible OpenAI → Clé**.
4. Consultez les modèles disponibles sur la [**page des modèles OpenRouter**](https://openrouter.ai/models).
5. Saisissez le nom du modèle dans **Serveur compatible OpenAI → Modèle**.
6. Saisissez `https://openrouter.ai/api/v1` dans **Serveur compatible OpenAI → URL de base**.
### Opérations DAW de base
Commandes du chat : `/clear`, `/welcome`, `/help`, `/hotkeys`
- Pistes
- Ajouter, renommer et réordonner les pistes depuis le panneau dinfos de piste.
- Changer dinstrument via le bouton instrument ; régler Solo, Mute et Volume.
- Supprimer une piste depuis son menu de réglages.
- **Enregistrement audio** : cliquez sur `Rec` dans la barre doutils pour enregistrer directement dans une piste audio.
- Régions
- Créer une région : avec loutil pointeur, double-cliquez ; ou maintenez `Ctrl/Cmd` et cliquez. Avec loutil crayon, un simple clic suffit.
- Déplacer / redimensionner : faites glisser le corps de région pour déplacer, ou les bords pour redimensionner.
- Ouvrir le Piano Roll via le petit crayon en haut à gauche de la région.
- Piano Roll
- Outils : Sélection ou Crayon.
- Créer des notes : double-clic ou `Ctrl/Cmd+clic` en mode sélection ; clic simple en mode crayon.
- Sélectionner : clic, `Maj+clic` pour la multisélection, ou glisser pour un lasso.
- Déplacer / redimensionner : faites glisser le corps ou les bords des notes.
- **Vue partition** : bascule entre Piano Roll et notation sur portée depuis la barre doutils.
- **Voies dautomation** : éditez le pitch bend et les courbes MIDI CC sous la grille piano.
- **Mode spectrogramme** : affichez le spectrogramme dune région audio comme référence d’édition.
- **Liste des événements** : inspectez et modifiez les notes, pitch bends et contrôleurs de la région MIDI active.
- Système de pistes globales
- Quatre pistes permanentes en haut de la timeline : **Repère**, **Tempo**, **Armure** et **Accord**.
- Elles pilotent le timing, les suggestions daccords, laffichage de la partition et certains résultats danalyse.
- Analyse audio
- **Détecter les accords** : ouvre lanalyse harmonique dune région audio et alimente la piste globale des accords.
- **Détecter le tempo** : analyse une région audio pour en extraire le BPM et peut réaligner le tempo du projet.
- Générateur musical K.G.One
- Cliquez sur le bouton **✦** dans la barre doutils.
- **Morceau complet** : génération dun titre complet à partir dune description et, si besoin, de paroles.
- **Clip** : génération de clips instrumentaux courts et de boucles MIDI.
- **Séparation de stems (navigateur)** : séparation locale dune région audio en stems, sans serveur.
- Snap et quantification
- Réglez le snap depuis le menu `NO SNAP`.
- Quantifiez avec `Qua. Pos.` et `Qua. Len.`.
- Lecture
- Retour au début ; Lecture / Pause depuis la barre doutils.
- Placez la tête de lecture en cliquant sur les numéros de mesure.
- Changez BPM, chiffrage de mesure et armure depuis la barre doutils.
---
### Remarque
Par sécurité, lorsque K.G.Studio tourne hors dun hôte local, les clés API ne sont pas conservées dans IndexedDB par défaut. Vous pouvez activer leur persistance dans les réglages, mais ce nest pas recommandé sur un environnement partagé.
### Avertissement
K.G.Studio nhéberge aucun des modèles mentionnés et nest affilié à aucun fournisseur de modèles. Toutes les données restent sur votre appareil ; vous êtes responsable des informations envoyées à des services tiers.
+59
View File
@@ -0,0 +1,59 @@
## Raccourcis
Ce guide récapitule les raccourcis clavier et les actions avec modificateurs actuellement disponibles dans K.G.Studio.
### Création selon la plateforme
- Sur **macOS**, utilisez `Cmd+clic` pour créer une région, une note ou un point dautomation avec loutil pointeur.
- Sur **Windows/Linux**, utilisez `Ctrl+clic` pour créer une région, une note ou un point dautomation avec loutil pointeur.
- Dans la timeline principale, un double-clic dans une zone vide crée une région lorsque loutil pointeur est actif.
- Dans le Piano Roll et les voies dautomation, loutil crayon permet aussi de créer avec un simple clic.
### Raccourcis principaux
- `Espace` — Lecture / Arrêt
- `C` — Activer / désactiver la boucle
- `R` — Démarrer / arrêter lenregistrement
- `Cmd/Ctrl+Z` — Annuler
- `Cmd/Ctrl+Shift+Z` — Rétablir
- `Cmd/Ctrl+C` — Copier les éléments sélectionnés
- `Cmd/Ctrl+V` — Coller
- `Cmd/Ctrl+S` — Enregistrer le projet
- `Cmd/Ctrl+A` — Sélectionner toutes les notes de la région MIDI active
- `Cmd/Ctrl+T` — Couper la région sélectionnée à la tête de lecture
- `Cmd/Ctrl+J` — Fusionner les régions MIDI sélectionnées
### Fenêtre de région / Piano Roll
- `E` — Ouvrir le Piano Roll pour une région MIDI sélectionnée, ou l’éditeur de forme donde pour une région audio sélectionnée. Si l’éditeur est déjà ouvert, `E` le ferme.
- `N` — Ouvrir le Piano Roll en vue partition pour une région MIDI sélectionnée.
- `Suppr` / `Retour arrière` — Supprimer les régions, notes ou points dautomation sélectionnés.
- `Esc` — Fermer la fenêtre du Piano Roll.
### Sélection
- `Maj+clic` — Ajouter à la sélection.
- Lasso dans une zone vide — Sélection rectangulaire de régions ou de points dautomation.
- `Maj+lasso` dans la fenêtre Piano Roll — Basculer la sélection des éléments contenus dans la zone.
### Raccourcis du Piano Roll
- `G` — Faire défiler les modes du guide daccords
- `Tab` — Aller à la suggestion daccord suivante
- `Maj+Tab` — Revenir à la suggestion daccord ou au renversement précédent
### Raccourcis de snap
- `1` — Aucun snap
- `2` — Snap sur `1/4`
- `3` — Snap sur `1/8`
- `4` — Snap sur `1/16`
### Raccourcis de quantification
- `5` — Quantification de position sur `1/4`
- `6` — Quantification de position sur `1/8`
- `7` — Quantification de position sur `1/16`
- `8` — Quantification de durée sur `1/4`
- `9` — Quantification de durée sur `1/8`
- `0` — Quantification de durée sur `1/16`
+11
View File
@@ -0,0 +1,11 @@
## Bon retour
Ravi de vous revoir. Votre fournisseur LLM semble déjà configuré, vous pouvez commencer à dialoguer immédiatement.
Conseils :
- Utilisez `/clear` à tout moment pour réinitialiser le chat.
- Tapez `/welcome` pour réafficher ce message.
- Tapez `/help` pour ouvrir laide.
- Tapez `/hotkeys` pour afficher le guide des raccourcis.
Bonne composition avec lassistant musical K.G.Studio.
+29
View File
@@ -0,0 +1,29 @@
## Mode LLM local
Bienvenue dans **lassistant musical K.G.Studio** en mode LLM local.
- Aucune API externe nest nécessaire. Tout sexécute directement dans votre navigateur, sans coût API supplémentaire.
- Ce mode utilise **Gemma 4 E4B** via **LiteRT-LM** avec accélération **WebGPU**.
- Configuration matérielle recommandée : GPU avec au moins **8 Go de VRAM** ou système avec au moins **16 Go de mémoire unifiée**.
- Les performances restent plus limitées que celles des grands modèles hébergés dans le cloud, surtout pour les tâches longues, complexes ou très itératives.
### Flux de travail recommandé
- Gardez des demandes courtes et ciblées.
- Faites progresser le modèle étape par étape vers le résultat final.
- Travaillez sur de petites régions musicales plutôt que sur des arrangements de morceau complet.
- Préférez des consignes darrangement simples lorsque cest possible.
- Ouvrez une nouvelle conversation pour chaque tâche autonome.
Exemple :
- Pour de meilleurs résultats, découpez les demandes complexes en plusieurs étapes.
- Si vous voulez que le modèle écrive une progression daccords à partir de la musique actuelle, commencez par : `Please read the current music.`
- Une fois cette étape terminée, poursuivez avec : `Please write a chord progression based on it.`
- Ce flux incrémental est généralement plus fiable en mode LLM local.
- Gardez la région MIDI que vous souhaitez modifier sélectionnée pendant tout le flux de travail.
- Il nest pas nécessaire de changer de sélection vers une autre région uniquement pour `read_music`, car `read_music` peut lire la musique sur lensemble des pistes.
### Utiliser plutôt un LLM externe
- Si vous souhaitez utiliser un modèle plus grand dans le cloud ou auto-hébergé, ouvrez **Réglages -> Général -> Fournisseur LLM** et quittez **LLM local (navigateur)**.
- Pour un modèle cloud, vous pouvez utiliser **OpenAI**, ou **Serveur compatible OpenAI** avec un fournisseur comme OpenRouter.
- Pour un modèle auto-hébergé, choisissez **Serveur compatible OpenAI** puis renseignez l**URL de base** et le **Modèle** de votre serveur.
- Après changement de fournisseur, démarrez une nouvelle conversation pour repartir proprement avec le nouveau modèle.
+25
View File
@@ -0,0 +1,25 @@
## Bienvenue
Bienvenue dans **lassistant musical K.G.Studio**, votre partenaire IA dans le navigateur pour la composition et larrangement.
### Démarrage rapide
**Configurer lassistant musical K.G.Studio**
- [Cliquez ici pour obtenir une clé API OpenRouter gratuite](https://openrouter.ai/keys) ; un compte OpenRouter peut être nécessaire.
- Dans **Réglages ⚙️ → Général → Fournisseur LLM**, sélectionnez **Serveur compatible OpenAI**.
- Dans **Serveur compatible OpenAI → Clé**, collez votre clé. Remarque : hors localhost, la clé nest pas conservée après rechargement.
- Dans **Serveur compatible OpenAI → Modèle**, saisissez `qwen/qwen3-30b-a3b:free`.
- Dans **Serveur compatible OpenAI → URL de base**, saisissez `https://openrouter.ai/api/v1`.
*(Vous pouvez aussi utiliser lAPI officielle OpenAI ou tout autre service compatible OpenAI.)*
**Opérations DAW de base**
- Double-cliquez sur une piste, ou faites `Ctrl/Cmd+clic`, pour créer une région.
- Faites glisser les bords dune région pour la redimensionner ; faites glisser son corps pour la déplacer.
- Cliquez sur le petit crayon en haut à gauche dune région pour ouvrir le Piano Roll.
- Dans le Piano Roll, double-cliquez ou faites `Ctrl/Cmd+clic` pour créer une note.
- Cliquez pour sélectionner ; `Maj+clic` pour la multisélection ; glissez pour une sélection rectangulaire.
- Faites glisser les bords dune note pour modifier sa durée, ou son corps pour la déplacer.
- Utilisez le snap dans la barre doutils du Piano Roll pour quantifier sur la grille.
### Avertissement
K.G.Studio nest affilié à aucun fournisseur LLM. Toutes les données sont stockées localement sur votre appareil ; vous restez responsable des données envoyées à des services tiers.
+8 -1
View File
@@ -3,6 +3,7 @@ import { beforeAll, describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import ChatBox from './ChatBox'; import ChatBox from './ChatBox';
import { I18nContext } from '../i18n/I18nProvider'; import { I18nContext } from '../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../i18n/types';
import { translate } from '../i18n/translate'; import { translate } from '../i18n/translate';
vi.mock('./chat', () => ({ vi.mock('./chat', () => ({
@@ -113,7 +114,7 @@ vi.mock('./common/KGDropdown', () => ({
default: () => null, default: () => null,
})); }));
function renderWithLocale(resolvedLocale: 'en_us' | 'zh_cn') { function renderWithLocale(resolvedLocale: ResolvedLocaleCode) {
return render( return render(
<I18nContext.Provider <I18nContext.Provider
value={{ value={{
@@ -144,4 +145,10 @@ describe('ChatBox', () => {
expect(screen.getByRole('heading', { level: 3, name: 'K.G.Studio 音乐创作助手' })).toBeTruthy(); 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'; } from '../test/utils/mock-data';
import { showAlert } from '../util/dialogUtil'; import { showAlert } from '../util/dialogUtil';
import { I18nContext } from '../i18n/I18nProvider'; import { I18nContext } from '../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../i18n/types';
import { translate } from '../i18n/translate'; import { translate } from '../i18n/translate';
const clickDropdownOption = (label: string) => { const clickDropdownOption = (label: string) => {
@@ -187,7 +188,7 @@ vi.mock('../core/KGCore', () => ({
})); }));
describe('EventListPanel', () => { describe('EventListPanel', () => {
const renderWithLocale = (locale: 'en_us' | 'zh_cn' = 'en_us') => render( const renderWithLocale = (locale: ResolvedLocaleCode = 'en_us') => render(
<I18nContext.Provider <I18nContext.Provider
value={{ value={{
languageSetting: locale, languageSetting: locale,
+2 -1
View File
@@ -4,6 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import InstrumentSelection from './InstrumentSelection'; import InstrumentSelection from './InstrumentSelection';
import { KGMidiTrack } from '../core/track/KGMidiTrack'; import { KGMidiTrack } from '../core/track/KGMidiTrack';
import { I18nContext } from '../i18n/I18nProvider'; import { I18nContext } from '../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../i18n/types';
import { translate } from '../i18n/translate'; import { translate } from '../i18n/translate';
const midiTrack = new KGMidiTrack('Lead Track', 1, 'acoustic_grand_piano'); const midiTrack = new KGMidiTrack('Lead Track', 1, 'acoustic_grand_piano');
@@ -19,7 +20,7 @@ vi.mock('../stores/projectStore', () => ({
useProjectStore: () => storeState, useProjectStore: () => storeState,
})); }));
function renderWithLocale(resolvedLocale: 'en_us' | 'zh_cn') { function renderWithLocale(resolvedLocale: ResolvedLocaleCode) {
return render( return render(
<I18nContext.Provider <I18nContext.Provider
value={{ value={{
+2 -1
View File
@@ -4,6 +4,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import PianoKeys from './PianoKeys'; import PianoKeys from './PianoKeys';
import { createMockMidiNote, createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data'; import { createMockMidiNote, createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
import { I18nContext } from '../../i18n/I18nProvider'; import { I18nContext } from '../../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../../i18n/types';
import { translate } from '../../i18n/translate'; import { translate } from '../../i18n/translate';
type TestLiveNoteActivityListener = (...args: [{ pitch: number; isNoteOn: boolean }]) => void; type TestLiveNoteActivityListener = (...args: [{ pitch: number; isNoteOn: boolean }]) => void;
@@ -52,7 +53,7 @@ vi.mock('../../core/midi-input/KGMidiInput', () => ({
function renderWithLocale( function renderWithLocale(
ui: React.ReactElement, ui: React.ReactElement,
resolvedLocale: 'en_us' | 'zh_cn' = 'en_us', resolvedLocale: ResolvedLocaleCode = 'en_us',
) { ) {
return render( return render(
<I18nContext.Provider <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 { KGPianoRollState, PIANO_ROLL_NO_SNAP } from '../../core/state/KGPianoRollState';
import { createMockMidiControllerEvent, createMockMidiPitchBend, createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data'; import { createMockMidiControllerEvent, createMockMidiPitchBend, createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
import { I18nContext } from '../../i18n/I18nProvider'; import { I18nContext } from '../../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../../i18n/types';
import { translate } from '../../i18n/translate'; import { translate } from '../../i18n/translate';
const coreMock = { const coreMock = {
@@ -47,7 +48,7 @@ import PianoRollAutomationLane from './PianoRollAutomationLane';
import { getControllerNumberForAutomationType } from './pianoRollAutomation'; import { getControllerNumberForAutomationType } from './pianoRollAutomation';
describe('PianoRollAutomationLane', () => { 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 <I18nContext.Provider
value={{ value={{
languageSetting: locale, languageSetting: locale,
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import PianoRollToolbar from './PianoRollToolbar'; import PianoRollToolbar from './PianoRollToolbar';
import { I18nContext } from '../../i18n/I18nProvider'; import { I18nContext } from '../../i18n/I18nProvider';
import type { ResolvedLocaleCode } from '../../i18n/types';
import { translate } from '../../i18n/translate'; import { translate } from '../../i18n/translate';
vi.mock('../common', () => ({ vi.mock('../common', () => ({
@@ -42,7 +43,7 @@ vi.mock('../../core/KGCore', () => ({
})); }));
describe('PianoRollToolbar', () => { 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 <I18nContext.Provider
value={{ value={{
languageSetting: locale, languageSetting: locale,
@@ -89,15 +89,15 @@ vi.mock('../../../util/local-separator/modelCache', () => ({
})); }));
describe('GeneralSettings', () => { describe('GeneralSettings', () => {
const renderSettings = () => render( const renderSettings = (locale: 'en_us' | 'zh_cn' = 'en_us') => render(
<I18nContext.Provider <I18nContext.Provider
value={{ value={{
languageSetting: 'auto', languageSetting: 'auto',
resolvedLocale: 'en_us', resolvedLocale: locale,
setLanguageSetting: async (value) => { setLanguageSetting: async (value) => {
await configManagerMock.set('general.language', value); await configManagerMock.set('general.language', value);
}, },
t: (key, params) => translate(key, params, 'en_us'), t: (key, params) => translate(key, params, locale),
}} }}
> >
<GeneralSettings /> <GeneralSettings />
@@ -137,6 +137,24 @@ describe('GeneralSettings', () => {
expect(screen.getByText(/require more VRAM/i)).toBeTruthy(); 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 () => { it('initializes the local context length from config', async () => {
renderSettings(); renderSettings();
@@ -251,11 +269,12 @@ describe('GeneralSettings', () => {
const languageSelect = await screen.findByLabelText('Language'); const languageSelect = await screen.findByLabelText('Language');
expect(languageSelect).toBeTruthy(); expect(languageSelect).toBeTruthy();
expect(screen.getAllByRole('combobox')[0]).toBe(languageSelect); 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(() => { 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, LOCAL_SEPARATOR_MODEL_IDS,
} from '../../../util/local-separator/config'; } 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 GeneralSettings: React.FC = () => {
const { t, setLanguageSetting } = useI18n(); const { t, setLanguageSetting } = useI18n();
const [language, setLanguage] = useState<LanguageSetting>('auto'); const [language, setLanguage] = useState<LanguageSetting>('auto');
@@ -352,9 +359,10 @@ const GeneralSettings: React.FC = () => {
onChange={(e) => void handleLanguageChange(e.target.value as LanguageSetting)} onChange={(e) => void handleLanguageChange(e.target.value as LanguageSetting)}
> >
<option value="auto">{t('settings.general.language.auto')}</option> <option value="auto">{t('settings.general.language.auto')}</option>
<option value="en_us">{t('settings.general.language.en_us')}</option> <option value="en_us">{LANGUAGE_OPTION_LABELS.en_us}</option>
<option value="zh_cn">{t('settings.general.language.zh_cn')}</option> <option value="fr_fr">{LANGUAGE_OPTION_LABELS.fr_fr}</option>
<option value="zh_hk">{t('settings.general.language.zh_hk')}</option> <option value="zh_cn">{LANGUAGE_OPTION_LABELS.zh_cn}</option>
<option value="zh_hk">{LANGUAGE_OPTION_LABELS.zh_hk}</option>
</select> </select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}> <div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{t('settings.general.language.help')} {t('settings.general.language.help')}
+19
View File
@@ -85,4 +85,23 @@ describe('I18nProvider', () => {
expect(screen.getByText('Settings')).toBeTruthy(); 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', () => { describe('instrument i18n helpers', () => {
it('returns translated instrument display names for supported locales', () => { 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, '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('原声大钢琴'); expect(getInstrumentDisplayName('acoustic_grand_piano', (key, params) => translate(key, params, 'zh_cn'))).toBe('原声大钢琴');
}); });
it('returns translated instrument group labels for supported locales', () => { 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, '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('钢琴与键盘'); 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', () => { it('normalizes supported language settings', () => {
expect(normalizeLanguageSetting('auto')).toBe('auto'); expect(normalizeLanguageSetting('auto')).toBe('auto');
expect(normalizeLanguageSetting('en_us')).toBe('en_us'); expect(normalizeLanguageSetting('en_us')).toBe('en_us');
expect(normalizeLanguageSetting('fr_fr')).toBe('fr_fr');
expect(normalizeLanguageSetting('zh_cn')).toBe('zh_cn'); expect(normalizeLanguageSetting('zh_cn')).toBe('zh_cn');
expect(normalizeLanguageSetting('zh_hk')).toBe('zh_hk'); expect(normalizeLanguageSetting('zh_hk')).toBe('zh_hk');
expect(normalizeLanguageSetting('unknown')).toBe('auto'); 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'); 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', () => { it('resolves auto to Simplified Chinese for Simplified Chinese locales', () => {
expect(resolveLanguageSetting('auto', ['zh-CN'])).toBe('zh_cn'); expect(resolveLanguageSetting('auto', ['zh-CN'])).toBe('zh_cn');
expect(resolveLanguageSetting('auto', ['zh-SG'])).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', () => { it('falls back to English when locale list is empty or unknown', () => {
expect(resolveLanguageSetting('auto', [])).toBe('en_us'); 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', () => { it('keeps explicit locale selections', () => {
expect(resolveLanguageSetting('en_us', ['zh-CN'])).toBe('en_us'); 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_cn', ['en-US'])).toBe('zh_cn');
expect(resolveLanguageSetting('zh_hk', ['en-US'])).toBe('zh_hk'); expect(resolveLanguageSetting('zh_hk', ['en-US'])).toBe('zh_hk');
expect(resolveLanguageSetting('zh_hk', ['zh-CN'])).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'; import type { LanguageSetting, ResolvedLocaleCode } from './types';
const CHINESE_LANGUAGE_PREFIX = 'zh'; const CHINESE_LANGUAGE_PREFIX = 'zh';
const FRENCH_LANGUAGE_PREFIX = 'fr';
const DEFAULT_LOCALE: ResolvedLocaleCode = 'en_us'; const DEFAULT_LOCALE: ResolvedLocaleCode = 'en_us';
// BCP 47 subtags that indicate Traditional Chinese (Hant script or HK/TW/MO regions) // BCP 47 subtags that indicate Traditional Chinese (Hant script or HK/TW/MO regions)
const TRADITIONAL_CHINESE_SUBTAGS = new Set(['hk', 'tw', 'mo', 'hant']); const TRADITIONAL_CHINESE_SUBTAGS = new Set(['hk', 'tw', 'mo', 'hant']);
export function normalizeLanguageSetting(value: unknown): LanguageSetting { 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[] { export function getBrowserLocales(): string[] {
@@ -28,7 +29,7 @@ export function resolveLanguageSetting(
setting: LanguageSetting, setting: LanguageSetting,
browserLocales: string[] = getBrowserLocales(), browserLocales: string[] = getBrowserLocales(),
): ResolvedLocaleCode { ): 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; return setting;
} }
@@ -42,6 +43,9 @@ export function resolveLanguageSetting(
const isTraditional = subtags.some((tag) => TRADITIONAL_CHINESE_SUBTAGS.has(tag)); const isTraditional = subtags.some((tag) => TRADITIONAL_CHINESE_SUBTAGS.has(tag));
return isTraditional ? 'zh_hk' : 'zh_cn'; return isTraditional ? 'zh_hk' : 'zh_cn';
} }
if (normalizedLocale === FRENCH_LANGUAGE_PREFIX || normalizedLocale.startsWith(`${FRENCH_LANGUAGE_PREFIX}-`)) {
return 'fr_fr';
}
} }
return DEFAULT_LOCALE; 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.help': 'Auto uses your system language.',
'settings.general.language.auto': 'Auto', 'settings.general.language.auto': 'Auto',
'settings.general.language.en_us': 'English', 'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': 'Simplified Chinese', 'settings.general.language.fr_fr': 'Français',
'settings.general.language.zh_hk': 'Traditional Chinese', 'settings.general.language.zh_cn': '简体中文',
'settings.general.language.zh_hk': '繁體中文',
'settings.yes': 'Yes', 'settings.yes': 'Yes',
'settings.no': 'No', 'settings.no': 'No',
'settings.restoreDefault': 'Restore default', '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 type { ResolvedLocaleCode, TranslationMessages } from '../types';
import { enUsMessages } from './en_us'; import { enUsMessages } from './en_us';
import { frFrMessages } from './fr_fr';
import { zhCnMessages } from './zh_cn'; import { zhCnMessages } from './zh_cn';
import { zhHkMessages } from './zh_hk'; import { zhHkMessages } from './zh_hk';
export const messagesByLocale: Record<ResolvedLocaleCode, TranslationMessages> = { export const messagesByLocale: Record<ResolvedLocaleCode, TranslationMessages> = {
en_us: enUsMessages, en_us: enUsMessages,
fr_fr: frFrMessages,
zh_cn: zhCnMessages, zh_cn: zhCnMessages,
zh_hk: zhHkMessages, zh_hk: zhHkMessages,
}; };
+2 -3
View File
@@ -1,6 +1,8 @@
import type { TranslationMessages } from '../types'; import type { TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
export const zhCnMessages: TranslationMessages = { export const zhCnMessages: TranslationMessages = {
...enUsMessages,
'app.loading': '加载中...', 'app.loading': '加载中...',
'assistant.displayName': 'K.G.Studio 音乐创作助手', 'assistant.displayName': 'K.G.Studio 音乐创作助手',
'assistant.welcomeFallback': '欢迎使用 K.G.Studio 音乐创作助手。', 'assistant.welcomeFallback': '欢迎使用 K.G.Studio 音乐创作助手。',
@@ -16,9 +18,6 @@ export const zhCnMessages: TranslationMessages = {
'settings.general.language.label': '语言', 'settings.general.language.label': '语言',
'settings.general.language.help': '自动会使用你的系统语言。', 'settings.general.language.help': '自动会使用你的系统语言。',
'settings.general.language.auto': '自动', 'settings.general.language.auto': '自动',
'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': '简体中文',
'settings.general.language.zh_hk': '繁体中文',
'settings.yes': '是', 'settings.yes': '是',
'settings.no': '否', 'settings.no': '否',
'settings.restoreDefault': '恢复默认', 'settings.restoreDefault': '恢复默认',
+2 -3
View File
@@ -1,6 +1,8 @@
import type { TranslationMessages } from '../types'; import type { TranslationMessages } from '../types';
import { enUsMessages } from './en_us';
export const zhHkMessages: TranslationMessages = { export const zhHkMessages: TranslationMessages = {
...enUsMessages,
'app.loading': '加載中...', 'app.loading': '加載中...',
'assistant.displayName': 'K.G.Studio 音樂創作助手', 'assistant.displayName': 'K.G.Studio 音樂創作助手',
'assistant.welcomeFallback': '歡迎使用 K.G.Studio 音樂創作助手。', 'assistant.welcomeFallback': '歡迎使用 K.G.Studio 音樂創作助手。',
@@ -16,9 +18,6 @@ export const zhHkMessages: TranslationMessages = {
'settings.general.language.label': '語言', 'settings.general.language.label': '語言',
'settings.general.language.help': '自動會使用你的系統語言。', 'settings.general.language.help': '自動會使用你的系統語言。',
'settings.general.language.auto': '自動', 'settings.general.language.auto': '自動',
'settings.general.language.en_us': 'English',
'settings.general.language.zh_cn': '簡體中文',
'settings.general.language.zh_hk': '繁體中文',
'settings.yes': '是', 'settings.yes': '是',
'settings.no': '否', 'settings.no': '否',
'settings.restoreDefault': '恢復默認', 'settings.restoreDefault': '恢復默認',
+5 -2
View File
@@ -2,18 +2,21 @@ import { describe, expect, it } from 'vitest';
import { translate } from './translate'; import { translate } from './translate';
describe('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, '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('自动'); 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('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'); expect(translate('nonexistent.key', undefined, 'zh_cn')).toBe('nonexistent.key');
}); });
it('interpolates params', () => { it('interpolates params', () => {
expect(translate('toolbar.status.bpmChanged', { value: 120 }, 'en_us')).toBe('BPM changed to 120'); 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'); 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'>; export type ResolvedLocaleCode = Exclude<LanguageSetting, 'auto'>;
@@ -111,6 +111,17 @@ describe('processUserMessage slash commands', () => {
expect(result.pseudoAssistantResponse).toContain('welcome_local_llm-zh_cn.md'); 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 () => { it('falls back to a localized welcome string when welcome markdown fetch fails', async () => {
configState.set('general.language', 'zh_cn'); configState.set('general.language', 'zh_cn');
configState.set('general.llm_provider', 'local_browser'); 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'); 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 () => { it('fetches the localized help guide for /help under zh-CN', async () => {
configState.set('general.language', 'zh_cn'); configState.set('general.language', 'zh_cn');
@@ -185,6 +209,15 @@ describe('processUserMessage slash commands', () => {
expect(result.pseudoAssistantResponse).toContain('chat/help-zh_cn.md'); 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 () => { it('falls back to the English help guide when the localized file is missing', async () => {
configState.set('general.language', 'zh_cn'); configState.set('general.language', 'zh_cn');
vi.stubGlobal('fetch', vi.fn(async (input: string | URL | Request) => { 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'); 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 () => { it('fetches the localized hotkeys guide for /hotkeys under zh-CN', async () => {
configState.set('general.language', 'zh_cn'); configState.set('general.language', 'zh_cn');