Add distribution metadata, Strobi app icons, CI, and comprehensive documentation. Migrate the project to pnpm and reorganize the Studio into domain-focused modules and components.
515 lines
22 KiB
TypeScript
515 lines
22 KiB
TypeScript
import {
|
||
createContext,
|
||
createElement,
|
||
useContext,
|
||
useEffect,
|
||
useState,
|
||
type ReactNode,
|
||
} from 'react'
|
||
import { chinese, translateChineseDynamicText } from './zh'
|
||
|
||
export type StudioLanguage = 'en' | 'fr' | 'zh-CN'
|
||
|
||
const STUDIO_LANGUAGE_STORAGE_KEY = 'avatar-studio-language'
|
||
|
||
const isStudioLanguage = (value: string | null): value is StudioLanguage =>
|
||
value === 'en' || value === 'fr' || value === 'zh-CN'
|
||
|
||
const readStoredStudioLanguage = (): StudioLanguage => {
|
||
if (typeof window === 'undefined') return 'en'
|
||
try {
|
||
const storedLanguage = window.localStorage.getItem(STUDIO_LANGUAGE_STORAGE_KEY)
|
||
return isStudioLanguage(storedLanguage) ? storedLanguage : 'en'
|
||
} catch {
|
||
return 'en'
|
||
}
|
||
}
|
||
|
||
const english: Record<string, string> = {
|
||
'Playground de l’avatar': 'Avatar playground',
|
||
Arrêté: 'Stopped',
|
||
'Aperçu procédural en direct': 'Live procedural preview',
|
||
'Aperçu de l’avatar': 'Avatar preview',
|
||
'Choisissez une animation exportée pour la lancer.': 'Choose an exported animation to play it.',
|
||
Lecture: 'Playback',
|
||
'Contrôlez l’animation active.': 'Control the active animation.',
|
||
Lire: 'Play',
|
||
Arrêter: 'Stop',
|
||
'API JavaScript': 'JavaScript API',
|
||
'Le module réutilisable est disponible dans avatar.js.':
|
||
'The reusable module is available as avatar.js.',
|
||
'Ouvrez cette démo directement, ou servez le dossier localement pour importer avatar.js depuis un autre module.':
|
||
'Open this demo directly, or serve the folder locally to import avatar.js from another module.',
|
||
'Langue de l’interface': 'Interface language',
|
||
'Nom de l’avatar': 'Avatar name',
|
||
sélecteur: 'picker',
|
||
hexadécimal: 'hex value',
|
||
'Lier width': 'Link widths',
|
||
'Lier height': 'Link heights',
|
||
'Lier size': 'Link sizes',
|
||
'Lier la position des yeux': 'Link eye positions',
|
||
'Glisser horizontalement pour modifier': 'Drag horizontally to adjust',
|
||
'En lecture': 'Playing',
|
||
'En pause': 'Paused',
|
||
'Réinitialiser la rotation de la tête': 'Reset head rotation',
|
||
'Réinitialiser la position et la rotation de': 'Reset position and rotation of',
|
||
'Avatar procédural': 'Procedural avatar',
|
||
'Gizmo de rotation': 'Rotation gizmo',
|
||
'Retour aux expressions': 'Back to expressions',
|
||
'Retour aux animations': 'Back to animations',
|
||
'Retour au studio': 'Back to studio',
|
||
'Preset en mémoire': 'Saved preset',
|
||
'Nouvelle expression': 'New expression',
|
||
'L’avatar à gauche affiche cette expression en direct.':
|
||
'The avatar on the left previews this expression live.',
|
||
Corps: 'Body',
|
||
'Apparence et orientation générale de l’avatar.': 'General avatar appearance and orientation.',
|
||
'Couleur du corps': 'Body color',
|
||
'Rotation de la tête': 'Head rotation',
|
||
Yeux: 'Eyes',
|
||
'Forme, placement et orientation propres au regard.': 'Eye shape, placement and orientation.',
|
||
'Couleur des yeux': 'Eye color',
|
||
Largeur: 'Width',
|
||
Hauteur: 'Height',
|
||
Échelle: 'Scale',
|
||
'Taille proportionnelle': 'Proportional size',
|
||
'Œil gauche': 'Left eye',
|
||
'Œil droit': 'Right eye',
|
||
'Position et espacement': 'Position and spacing',
|
||
Horizontale: 'Horizontal',
|
||
Verticale: 'Vertical',
|
||
Espacement: 'Spacing',
|
||
'Rotation locale': 'Local rotation',
|
||
Projection: 'Projection',
|
||
'Perspective appliquée à la surface active.': 'Perspective applied to the active surface.',
|
||
Perspective: 'Perspective',
|
||
Supprimer: 'Delete',
|
||
Enregistrer: 'Save',
|
||
'Construction du corps': 'Body construction',
|
||
'Choisis la forme principale puis assemble les primitives autour d’elle.':
|
||
'Choose the primary shape, then assemble primitives around it.',
|
||
'Choisir un avatar': 'Choose an avatar',
|
||
'Double-clic pour modifier': 'Double-click to edit',
|
||
'Nouvel avatar': 'New avatar',
|
||
Modifier: 'Edit',
|
||
'Mode d’édition': 'Editing mode',
|
||
Pose: 'Pose',
|
||
Expressions: 'Expressions',
|
||
Animations: 'Animations',
|
||
Exporter: 'Export',
|
||
'Exporter l’avatar': 'Export avatar',
|
||
'Télécharge un composant autonome avec les animations de ton choix.':
|
||
'Download a standalone component with the animations you choose.',
|
||
'Avatar sélectionné': 'Selected avatar',
|
||
Format: 'Format',
|
||
'Choisis l’intégration correspondant à ton projet.':
|
||
'Choose the integration that matches your project.',
|
||
'Composant TSX autonome': 'Standalone TSX component',
|
||
'Package React local (.zip)': 'Local React package (.zip)',
|
||
'Module JavaScript': 'JavaScript module',
|
||
'Module ES autonome': 'Standalone ES module',
|
||
'Projet HTML + module JS (.zip)': 'HTML project + JS module (.zip)',
|
||
sélectionnées: 'selected',
|
||
'Animations à exporter': 'Animations to export',
|
||
'Tout sélectionner': 'Select all',
|
||
'Tout désélectionner': 'Deselect all',
|
||
'Télécharger le composant TSX': 'Download TSX component',
|
||
'Télécharger le package React': 'Download React package',
|
||
'Télécharger le module': 'Download module',
|
||
Snapshot: 'Snapshot',
|
||
'Mode photo': 'Photo Mode',
|
||
'Capture une image statique de l’avatar.': 'Capture a static image of the avatar.',
|
||
'Les options de capture seront configurées ici.': 'Snapshot options will be configured here.',
|
||
'Aperçu du Snapshot': 'Snapshot preview',
|
||
'Aperçu du mode photo': 'Photo Mode preview',
|
||
'Arrière-plan': 'Background',
|
||
'Choisis un fond transparent, uni ou en dégradé.':
|
||
'Choose a transparent, solid or gradient background.',
|
||
Style: 'Style',
|
||
Transparent: 'Transparent',
|
||
Uni: 'Solid',
|
||
'Dégradé linéaire': 'Linear gradient',
|
||
'Dégradé radial': 'Radial gradient',
|
||
'Style d’arrière-plan': 'Background style',
|
||
Couleur: 'Color',
|
||
Départ: 'Start',
|
||
Arrivée: 'End',
|
||
Définition: 'Resolution',
|
||
'Dimensions inscrites dans le fichier SVG.': 'Dimensions embedded in the SVG file.',
|
||
'Dimensions du fichier exporté.': 'Dimensions of the exported file.',
|
||
'Définition du Snapshot': 'Snapshot resolution',
|
||
'Définition du mode photo': 'Photo Mode resolution',
|
||
'Télécharger le Snapshot SVG': 'Download SVG snapshot',
|
||
'Télécharger en SVG': 'Download SVG',
|
||
'Télécharger en PNG': 'Download PNG',
|
||
'Format d’export': 'Export format',
|
||
'Format d’export du mode photo': 'Photo Mode export format',
|
||
'Choisis le type de fichier généré par le mode photo.':
|
||
'Choose the file type generated by Photo Mode.',
|
||
'Prendre une photo': 'Take a picture',
|
||
'Informations sur le mode photo': 'Photo Mode information',
|
||
'Tu peux modifier le format, le fond et la définition du mode photo dans Export.':
|
||
'You can change the format, background and resolution for Photo Mode in Export.',
|
||
'Projet du Studio': 'Studio project',
|
||
'Transfère tous les avatars, expressions et animations vers un autre navigateur.':
|
||
'Transfer every avatar, expression and animation to another browser.',
|
||
'Télécharger le projet JSON': 'Download JSON project',
|
||
'Importer un projet JSON': 'Import JSON project',
|
||
'Ce fichier ne contient pas un projet Avatar Studio valide et compatible.':
|
||
'This file does not contain a valid, compatible Avatar Studio project.',
|
||
'Importer ce projet ?': 'Import this project?',
|
||
'Le projet local actuel sera remplacé par les avatars, expressions, animations et état de lecture de ce fichier.':
|
||
'The current local project will be replaced with the avatars, expressions, animations and playback state from this file.',
|
||
'Le projet n’a pas pu être enregistré dans ce navigateur. Libère de l’espace puis réessaie.':
|
||
'The project could not be saved in this browser. Free up some space and try again.',
|
||
Importer: 'Import',
|
||
'Construction, forme et couleur de la tête de l’avatar.':
|
||
'Build, shape and color of the avatar head.',
|
||
'Une forme principale porte les yeux. Les autres primitives se placent autour d’elle.':
|
||
'One primary shape carries the eyes. Other primitives are placed around it.',
|
||
'Forme principale': 'Primary shape',
|
||
'porte les yeux': 'carries the eyes',
|
||
'Ajouter une forme': 'Add a shape',
|
||
Dupliquer: 'Duplicate',
|
||
'Gizmo local': 'Local gizmo',
|
||
'Déplacer dans le plan de la caméra': 'Move in camera plane',
|
||
Transformer: 'Transform',
|
||
copie: 'copy',
|
||
'Glisse un axe pour déplacer la forme, ou un anneau pour la faire tourner.':
|
||
'Drag an axis to move the shape, or a ring to rotate it.',
|
||
Profondeur: 'Depth',
|
||
'Position locale': 'Local position',
|
||
'Cette surface est la référence du visage et porte les yeux.':
|
||
'This surface is the face reference and carries the eyes.',
|
||
'Rondeur des arêtes': 'Edge roundness',
|
||
Rondeur: 'Roundness',
|
||
'Rondeur morphologique': 'Morph roundness',
|
||
'Rondeur de la pointe': 'Tip roundness',
|
||
'Rondeur de la base': 'Base roundness',
|
||
'Rondeur globale': 'Global roundness',
|
||
'Rondeur pointe': 'Tip roundness',
|
||
'Rondeur base': 'Base roundness',
|
||
'Couleur de base utilisée par les poses et les expressions.':
|
||
'Base color used by poses and expressions.',
|
||
'Forme, placement, orientation et couleur du regard par défaut.':
|
||
'Default eye shape, placement, orientation and color.',
|
||
'Définis l’identité du regard de cet avatar. Les poses s’ajoutent ensuite à cette base.':
|
||
'Define this avatar’s default eyes. Poses are then applied on top of this base.',
|
||
'Coordonnées propres à l’avatar, indépendantes des poses.':
|
||
'Avatar-specific coordinates, independent from poses.',
|
||
'Inclinaison par défaut propre à chaque œil.': 'Default tilt for each eye.',
|
||
'Orientation et apparence générale de la pose.': 'General pose orientation and appearance.',
|
||
'La pose peut remplacer temporairement la couleur de l’avatar.':
|
||
'The pose can temporarily override the avatar color.',
|
||
'Reprendre la couleur de l’avatar': 'Use avatar color',
|
||
'Les libellés ↔ sont scrubbables, comme dans Figma.':
|
||
'Labels marked ↔ can be scrubbed, like in Figma.',
|
||
'Forme, placement, orientation et couleur du regard.':
|
||
'Eye shape, placement, orientation and color.',
|
||
'Coordonnées communes projetées sur la forme choisie.':
|
||
'Shared coordinates projected onto the selected shape.',
|
||
'Inclinaison propre à chaque œil.': 'Tilt applied to each eye.',
|
||
'Perspective et repères appliqués à la surface active.':
|
||
'Perspective and guides applied to the active surface.',
|
||
'Profondeur simulée du visage.': 'Simulated face depth.',
|
||
'Afficher le maillage': 'Show wireframe',
|
||
Réinitialiser: 'Reset',
|
||
Mouvement: 'Motion',
|
||
'Mouvement perpétuel': 'Perpetual motion',
|
||
'Aucun mouvement': 'No motion',
|
||
'Dérive lente': 'Slow drift',
|
||
'Micro-ajustements': 'Micro-adjustments',
|
||
Tremblement: 'Shake',
|
||
'Ajoute une légère présence ou un tremblement continu au corps.':
|
||
'Adds a subtle living presence or a continuous shake to the body.',
|
||
'Anime le regard par petites saccades naturelles ou par tremblement.':
|
||
'Animates the gaze with natural micro-saccades or a continuous shake.',
|
||
'Motion interpole les valeurs et notre moteur effectue le slerp quaternion.':
|
||
'Motion interpolates values while the engine performs quaternion slerp.',
|
||
'Vitesse du ressort': 'Spring speed',
|
||
'Une animation est en cours de lecture': 'An animation is currently playing',
|
||
'Mettez l’animation en pause avant de manipuler les paramètres de pose.':
|
||
'Pause the animation before adjusting pose parameters.',
|
||
'Mettez l’animation en pause avant de choisir ou personnaliser une expression.':
|
||
'Pause the animation before selecting or customizing an expression.',
|
||
Cligner: 'Blink',
|
||
'Expression aléatoire': 'Random expression',
|
||
'Cycle de vie': 'Life cycle',
|
||
Réactions: 'Reactions',
|
||
'Éditeur d’animation': 'Animation editor',
|
||
'Modifier l’animation': 'Edit animation',
|
||
'Nouvelle animation': 'New animation',
|
||
'Compose les expressions, leur cadence et les clignements de cette animation.':
|
||
'Compose the expressions, timing and blinks for this animation.',
|
||
'Prévisualiser l’animation': 'Preview animation',
|
||
'Arrêter l’animation': 'Stop animation',
|
||
Identité: 'Identity',
|
||
'Nom, catégorie et comportement de lecture de l’animation.':
|
||
'Animation name, category and playback behavior.',
|
||
Nom: 'Name',
|
||
Catégorie: 'Category',
|
||
Description: 'Description',
|
||
'Mode de lecture': 'Playback mode',
|
||
Boucle: 'Loop',
|
||
'Une fois': 'Once',
|
||
'Aller-retour': 'Ping-pong',
|
||
Timeline: 'Timeline',
|
||
'Glisse les étapes pour les réordonner, puis sélectionne-en une pour régler sa cadence.':
|
||
'Drag steps to reorder them, then select one to adjust its timing.',
|
||
'Ajoute une expression pour commencer.': 'Add an expression to get started.',
|
||
'Étape sélectionnée': 'Selected step',
|
||
'Durée visible avant de passer à l’expression suivante.':
|
||
'Visible duration before moving to the next expression.',
|
||
'Supprimer cette étape': 'Delete this step',
|
||
'Temps d’affichage': 'Display time',
|
||
'Durée de transition': 'Transition duration',
|
||
Transition: 'Transition',
|
||
Ressort: 'Spring',
|
||
Douce: 'Smooth',
|
||
Rapide: 'Snappy',
|
||
'Ajouter une expression': 'Add expression',
|
||
'Sélectionne un preset pour l’ajouter à la fin de la timeline.':
|
||
'Select a preset to append it to the timeline.',
|
||
Clignements: 'Blinks',
|
||
'Le blink fonctionne indépendamment des changements d’expression.':
|
||
'Blinking runs independently from expression changes.',
|
||
'Activer les clignements': 'Enable blinking',
|
||
'Intervalle minimum': 'Minimum interval',
|
||
'Intervalle maximum': 'Maximum interval',
|
||
'Intervalle du clignement': 'Blink interval',
|
||
'Durée du clignement': 'Blink duration',
|
||
animations: 'animations',
|
||
'Chaque étape possède sa propre durée et sa propre transition.':
|
||
'Each step has its own duration and transition.',
|
||
Désactivé: 'Disabled',
|
||
'comportement de la timeline': 'timeline behavior',
|
||
'Supprimer cette animation ?': 'Delete this animation?',
|
||
'Cette action supprimera définitivement cette animation.':
|
||
'This permanently deletes this animation.',
|
||
'Cette animation enchaîne un pool de presets et des clignements.':
|
||
'This animation cycles through presets and blinks.',
|
||
'Cet état enchaîne un pool de presets et des clignements.':
|
||
'This animation cycles through presets and blinks.',
|
||
'Expressions de l’animation': 'Animation expressions',
|
||
'Les presets sont joués dans cet ordre, puis la boucle recommence.':
|
||
'Presets play in this order, then the loop starts again.',
|
||
'Logique de clignement': 'Blink behavior',
|
||
'Le rythme reste naturel grâce à un intervalle légèrement aléatoire.':
|
||
'A slightly randomized interval keeps the rhythm natural.',
|
||
'Premier clignement': 'First blink',
|
||
'après le lancement': 'after launch',
|
||
Intervalle: 'Interval',
|
||
'tirage aléatoire': 'randomized',
|
||
Durée: 'Duration',
|
||
'fermeture et ouverture': 'close and open',
|
||
'Changement d’expression': 'Expression change',
|
||
'cadence de l’animation': 'animation tempo',
|
||
'Détails de l’animation': 'Animation details',
|
||
'Afficher les détails de l’animation': 'Show animation details',
|
||
'Masquer les détails de l’animation': 'Hide animation details',
|
||
Relancer: 'Restart',
|
||
Lancer: 'Play',
|
||
Pause: 'Pause',
|
||
Reprendre: 'Resume',
|
||
Annuler: 'Cancel',
|
||
'Supprimer cette expression ?': 'Delete this expression?',
|
||
'Cette action retirera définitivement le preset de la bibliothèque de cet avatar.':
|
||
'This permanently removes the preset from this avatar’s library.',
|
||
'Cette expression sera aussi retirée des animations suivantes :':
|
||
'This expression will also be removed from the following animations:',
|
||
'Si une animation ne contient que cette expression, l’expression de repli lui sera assignée pour qu’elle reste jouable.':
|
||
'If an animation only contains this expression, the fallback expression will be assigned so it remains playable.',
|
||
'Le corps, les expressions et les animations propres à cet avatar seront définitivement supprimés. La bibliothèque de base sera conservée.':
|
||
'This avatar’s body, expressions and animations will be permanently deleted. The base library will be preserved.',
|
||
'Glisse sur la surface pour orienter la tête. Les anneaux du gizmo contrôlent X, Y et Z.':
|
||
'Drag on the surface to orient the head. The gizmo rings control X, Y and Z.',
|
||
Sphère: 'Sphere',
|
||
Curseur: 'Cursor',
|
||
Cylindre: 'Cylinder',
|
||
Cône: 'Cone',
|
||
Diamant: 'Diamond',
|
||
'Yeux presque fermés, respiration lente et expression de sommeil.':
|
||
'Nearly closed eyes, slow breathing and a sleepy expression.',
|
||
'Animation courte de réveil avant retour vers une expression neutre.':
|
||
'Short waking animation before returning to a neutral expression.',
|
||
'Séquence courte de réveil avant retour vers une expression neutre.':
|
||
'Short waking animation before returning to a neutral expression.',
|
||
'Micro-mouvements lents, expressions 00 et 08, clignement rare.':
|
||
'Slow micro-movements, expressions 00 and 08, infrequent blinking.',
|
||
'Expressions 10, 01 et 19, regard stable et clignement attentif.':
|
||
'Expressions 10, 01 and 19, steady gaze and attentive blinking.',
|
||
'Regard haut et latéral, expressions asymétriques et changements fréquents.':
|
||
'Upward side gaze, asymmetric expressions and frequent changes.',
|
||
'Balayage rapide et changements très fréquents.': 'Fast scanning and very frequent changes.',
|
||
'Rythme régulier, regard concentré et micro-variations.':
|
||
'Steady rhythm, focused gaze and subtle variations.',
|
||
'Grandes expressions positives et mouvements rapides.':
|
||
'Big positive expressions and fast movements.',
|
||
'Inclinaisons et rotations rapides pour simuler un agent en mouvement.':
|
||
'Fast tilts and rotations to simulate a moving agent.',
|
||
'Rythme régulier et expressions concentrées.': 'Steady rhythm and focused expressions.',
|
||
'Grandes expressions et transitions rapides.': 'Big expressions and fast transitions.',
|
||
'Inclinaisons et forte asymétrie.': 'Tilts and strong asymmetry.',
|
||
sleeping: 'sleeping',
|
||
waking: 'waking',
|
||
idle: 'idle',
|
||
listening: 'listening',
|
||
thinking: 'thinking',
|
||
searching: 'searching',
|
||
working: 'working',
|
||
excited: 'excited',
|
||
surprised: 'surprised',
|
||
suspicious: 'suspicious',
|
||
angry: 'angry',
|
||
drowsy: 'drowsy',
|
||
happy: 'happy',
|
||
curious: 'curious',
|
||
confused: 'confused',
|
||
bored: 'bored',
|
||
proud: 'proud',
|
||
shy: 'shy',
|
||
sad: 'sad',
|
||
laughing: 'laughing',
|
||
scared: 'scared',
|
||
playful: 'playful',
|
||
celebrate: 'celebrate',
|
||
orbit: 'orbit',
|
||
radar: 'radar',
|
||
progress: 'progress',
|
||
spawning: 'spawning',
|
||
humming: 'humming',
|
||
loading: 'loading',
|
||
dictating: 'dictating',
|
||
writing: 'writing',
|
||
sending: 'sending',
|
||
receiving: 'receiving',
|
||
uploading: 'uploading',
|
||
notifying: 'notifying',
|
||
alerting: 'alerting',
|
||
dragging: 'dragging',
|
||
bouncing: 'bouncing',
|
||
'powering-down': 'powering down',
|
||
}
|
||
|
||
const frenchStates: Record<string, string> = {
|
||
loop: 'boucle',
|
||
once: 'une fois',
|
||
pingPong: 'aller-retour',
|
||
Custom: 'Personnalisé',
|
||
'Untitled animation': 'Animation sans titre',
|
||
// Compatibilité avec les animations intégrées déjà persistées avant le renommage.
|
||
'Untitled sequence': 'Animation sans titre',
|
||
'Cet état enchaîne un pool de presets et des clignements.':
|
||
'Cette animation enchaîne un pool de presets et des clignements.',
|
||
'Séquence courte de réveil avant retour vers une expression neutre.':
|
||
'Animation courte de réveil avant retour vers une expression neutre.',
|
||
sleeping: 'sommeil',
|
||
waking: 'réveil',
|
||
idle: 'au repos',
|
||
listening: 'écoute',
|
||
thinking: 'réflexion',
|
||
searching: 'recherche',
|
||
working: 'travail',
|
||
excited: 'enthousiaste',
|
||
surprised: 'surpris',
|
||
suspicious: 'méfiant',
|
||
angry: 'en colère',
|
||
drowsy: 'somnolent',
|
||
happy: 'heureux',
|
||
curious: 'curieux',
|
||
confused: 'confus',
|
||
bored: 'ennuyé',
|
||
proud: 'fier',
|
||
shy: 'timide',
|
||
sad: 'triste',
|
||
laughing: 'rire',
|
||
scared: 'effrayé',
|
||
playful: 'joueur',
|
||
celebrate: 'célébration',
|
||
orbit: 'orbite',
|
||
radar: 'radar',
|
||
progress: 'progression',
|
||
spawning: 'apparition',
|
||
humming: 'fredonnement',
|
||
loading: 'chargement',
|
||
dictating: 'dictée',
|
||
writing: 'écriture',
|
||
sending: 'envoi',
|
||
receiving: 'réception',
|
||
uploading: 'téléversement',
|
||
notifying: 'notification',
|
||
alerting: 'alerte',
|
||
dragging: 'glissement',
|
||
bouncing: 'rebond',
|
||
'powering-down': 'extinction',
|
||
}
|
||
|
||
const dynamicTranslations: [RegExp, string][] = [
|
||
[/^Modifier l’expression (.+)$/, 'Edit expression $1'],
|
||
[/^Modifier (.+)$/, 'Edit $1'],
|
||
[/^Supprimer (.+) \?$/, 'Delete $1?'],
|
||
[/^Ajouter une forme · (.+)$/, 'Add a shape · $1'],
|
||
[/^Réinitialiser la position et la rotation de (.+)$/, 'Reset position and rotation of $1'],
|
||
[/^Animation en cours : (.+)$/, 'Active animation: $1'],
|
||
[/^Mettre (.+) en pause$/, 'Pause $1'],
|
||
[/^Reprendre (.+)$/, 'Resume $1'],
|
||
[/^Arrêter (.+)$/, 'Stop $1'],
|
||
[/^(.+) expressions$/, '$1 expressions'],
|
||
]
|
||
|
||
export const translateStudioText = (text: string, language: StudioLanguage) => {
|
||
if (language === 'fr') return frenchStates[text] ?? text
|
||
if (language === 'zh-CN') {
|
||
const exact = chinese[text]
|
||
if (exact) return exact
|
||
const dynamic = translateChineseDynamicText(text)
|
||
if (dynamic) return dynamic
|
||
return Object.entries(chinese)
|
||
.sort(([left], [right]) => right.length - left.length)
|
||
.reduce(
|
||
(translated, [source, replacement]) => translated.replaceAll(source, replacement),
|
||
text
|
||
)
|
||
}
|
||
const exact = english[text]
|
||
if (exact) return exact
|
||
for (const [pattern, replacement] of dynamicTranslations) {
|
||
if (pattern.test(text)) return text.replace(pattern, replacement)
|
||
}
|
||
return Object.entries(english)
|
||
.sort(([left], [right]) => right.length - left.length)
|
||
.reduce((translated, [source, replacement]) => translated.replaceAll(source, replacement), text)
|
||
}
|
||
|
||
type StudioLanguageContextValue = {
|
||
language: StudioLanguage
|
||
setLanguage: (language: StudioLanguage) => void
|
||
t: (text: string) => string
|
||
}
|
||
|
||
const StudioLanguageContext = createContext<StudioLanguageContextValue | null>(null)
|
||
|
||
export function StudioLanguageProvider({ children }: { children: ReactNode }) {
|
||
const [language, setLanguage] = useState<StudioLanguage>(readStoredStudioLanguage)
|
||
|
||
useEffect(() => {
|
||
try {
|
||
window.localStorage.setItem(STUDIO_LANGUAGE_STORAGE_KEY, language)
|
||
} catch {
|
||
// The studio remains usable when browser storage is unavailable.
|
||
}
|
||
}, [language])
|
||
|
||
return createElement(
|
||
StudioLanguageContext.Provider,
|
||
{ value: { language, setLanguage, t: text => translateStudioText(text, language) } },
|
||
children
|
||
)
|
||
}
|
||
|
||
export const useStudioLanguage = () => {
|
||
const context = useContext(StudioLanguageContext)
|
||
if (!context) throw new Error('useStudioLanguage must be used inside StudioLanguageProvider')
|
||
return context
|
||
}
|