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 = { '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 = { 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(null) export function StudioLanguageProvider({ children }: { children: ReactNode }) { const [language, setLanguage] = useState(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 }