From a4fe0ce93f5aa49e32a7b286d77469c88b6663c1 Mon Sep 17 00:00:00 2001 From: youtsuho <110821381+youtsuhodev@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:28:30 +0200 Subject: [PATCH] feat(i18n): add French locale across the avatar editor Add fr as a first-class AvatarLocale alongside en and zh-Hans. - Register the fr locale and its native label (Francais) in AVATAR_LOCALES so the language switcher renders it automatically. - Add a complete FR_TRANSLATIONS dictionary covering every UI string that already had a Simplified Chinese translation, including all face presets and animal breed labels. - Extend normalizeAvatarLocale to resolve fr and fr-* variants, and route translateAvatarText to the French dictionary. French browser locales now resolve automatically on first load. - Add hasAvatarTranslation so coverage tests can assert an explicit entry even when a French label matches the English one, such as proper nouns like Husky or Corgi. - Widen the locale type to 'en' | 'zh-Hans' | 'fr' in the React renderer, the Web mount options and custom element attribute parsing, and the Vue editor prop, and translate the language name in the Chinese dictionary. - Extend the locale spec with French normalization, translation, and face-preset/breed coverage assertions. --- __tests__/avatarLocale.spec.ts | 35 +- packages/react/src/renderer.tsx | 2 +- packages/vue/src/index.ts | 2 +- packages/web/src/elements.ts | 2 +- packages/web/src/index.ts | 2 +- src/avatarLocale.tsx | 763 +++++++++++++++++++++++++++++++- 6 files changed, 796 insertions(+), 10 deletions(-) diff --git a/__tests__/avatarLocale.spec.ts b/__tests__/avatarLocale.spec.ts index feea916..5772742 100644 --- a/__tests__/avatarLocale.spec.ts +++ b/__tests__/avatarLocale.spec.ts @@ -1,15 +1,17 @@ import { describe, expect, it } from 'vitest' -import { normalizeAvatarLocale, translateAvatarText } from '../src/avatarLocale' +import { hasAvatarTranslation, normalizeAvatarLocale, translateAvatarText } from '../src/avatarLocale' import { AVATAR_FACE_PRESETS } from '../src/avatarFacePresets' import { AVATAR_ANIMAL_BREED_TEMPLATES } from '../src/avatarSpeciesBreeds' describe('avatar locale', () => { - it('normalizes supported English and Simplified Chinese locale variants', () => { + it('normalizes supported English, Simplified Chinese, and French locale variants', () => { expect(normalizeAvatarLocale('en')).toBe('en') expect(normalizeAvatarLocale('zh-CN')).toBe('zh-Hans') expect(normalizeAvatarLocale('zh-Hans')).toBe('zh-Hans') - expect(normalizeAvatarLocale('fr')).toBeNull() + expect(normalizeAvatarLocale('fr')).toBe('fr') + expect(normalizeAvatarLocale('fr-FR')).toBe('fr') + expect(normalizeAvatarLocale('de')).toBeNull() }) it('translates known labels and preserves unknown text', () => { @@ -88,6 +90,21 @@ describe('avatar locale', () => { expect(translateAvatarText('zh-Hans', 'Custom animation name')).toBe('Custom animation name') }) + it('translates known labels to French and preserves unknown text', () => { + expect(translateAvatarText('fr', 'Effects')).toBe('Effets') + expect(translateAvatarText('fr', 'Download PNG')).toBe('Télécharger le PNG') + expect(translateAvatarText('fr', 'Transparent')).toBe('Transparent') + expect(translateAvatarText('fr', 'Bring your avatar to life')).toBe('Donnez vie à votre avatar') + expect(translateAvatarText('fr', 'Start creating')).toBe('Commencer à créer') + expect(translateAvatarText('fr', 'Dog')).toBe('Chien') + expect(translateAvatarText('fr', 'Fox')).toBe('Renard') + expect(translateAvatarText('fr', 'Fox types')).toBe('Types de renards') + expect(translateAvatarText('fr', 'Red Fox')).toBe('Renard roux') + expect(translateAvatarText('fr', 'Language')).toBe('Langue') + expect(translateAvatarText('fr', 'French')).toBe('Français') + expect(translateAvatarText('fr', 'Custom animation name')).toBe('Custom animation name') + }) + it('provides Simplified Chinese labels for every face preset', () => { for (const preset of AVATAR_FACE_PRESETS) { expect(translateAvatarText('zh-Hans', preset.label)).not.toBe(preset.label) @@ -99,4 +116,16 @@ describe('avatar locale', () => { expect(translateAvatarText('zh-Hans', template.label)).not.toBe(template.label) } }) + + it('provides French labels for every face preset', () => { + for (const preset of AVATAR_FACE_PRESETS) { + expect(hasAvatarTranslation('fr', preset.label)).toBe(true) + } + }) + + it('provides French labels for every new animal breed', () => { + for (const template of AVATAR_ANIMAL_BREED_TEMPLATES) { + expect(hasAvatarTranslation('fr', template.label)).toBe(true) + } + }) }) diff --git a/packages/react/src/renderer.tsx b/packages/react/src/renderer.tsx index 149c8fa..5fd1b24 100644 --- a/packages/react/src/renderer.tsx +++ b/packages/react/src/renderer.tsx @@ -47,7 +47,7 @@ export type { } from '@oneworks/avatar' export type AvatarTheme = 'dark' | 'light' | 'system' -export type AvatarLocale = 'en' | 'zh-Hans' +export type AvatarLocale = 'en' | 'zh-Hans' | 'fr' export type AvatarCaptureOptions = { readonly background?: string | 'transparent' readonly format: 'png' | 'svg' diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 3019dde..e3b90b8 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -188,7 +188,7 @@ export const OneWorksAvatar = oneWorksAvatarComponent as typeof oneWorksAvatarCo const editorProps = { animationLibraries: { default: () => [], type: Array as PropType }, definition: { default: undefined, type: Object as PropType }, - locale: { default: 'en', type: String as PropType<'en' | 'zh-Hans'> }, + locale: { default: 'en', type: String as PropType<'en' | 'zh-Hans' | 'fr'> }, theme: { default: 'system', type: String as PropType } } diff --git a/packages/web/src/elements.ts b/packages/web/src/elements.ts index dd33813..848a06b 100644 --- a/packages/web/src/elements.ts +++ b/packages/web/src/elements.ts @@ -223,7 +223,7 @@ export class OneWorksAvatarEditorElement extends HTMLElementBase { return { animationLibraries: this.#animationLibraries, definition: this.#definition, - locale: locale === 'zh-Hans' ? locale : 'en', + locale: locale === 'zh-Hans' || locale === 'fr' ? locale : 'en', theme: theme === 'dark' || theme === 'light' ? theme : 'system' } } diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index f1a5abb..0c9ed14 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -72,7 +72,7 @@ export interface AvatarMountOptions { export interface AvatarEditorMountOptions { readonly animationLibraries?: readonly AvatarAnimationLibrary[] readonly definition?: AvatarDefinition - readonly locale?: 'en' | 'zh-Hans' + readonly locale?: 'en' | 'zh-Hans' | 'fr' readonly theme?: AvatarTheme } diff --git a/src/avatarLocale.tsx b/src/avatarLocale.tsx index cbe99f3..b7ba90d 100644 --- a/src/avatarLocale.tsx +++ b/src/avatarLocale.tsx @@ -1,7 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' -export type AvatarLocale = 'en' | 'zh-Hans' +export type AvatarLocale = 'en' | 'zh-Hans' | 'fr' export const AVATAR_LOCALES: readonly { readonly id: AvatarLocale @@ -9,7 +9,8 @@ export const AVATAR_LOCALES: readonly { readonly nativeLabel: string }[] = [ { id: 'zh-Hans', label: 'Simplified Chinese', nativeLabel: '简体中文' }, - { id: 'en', label: 'English', nativeLabel: 'English' } + { id: 'en', label: 'English', nativeLabel: 'English' }, + { id: 'fr', label: 'French', nativeLabel: 'Français' } ] const AVATAR_LOCALE_STORAGE_KEY = 'oneworks-avatar-locale' @@ -644,6 +645,7 @@ const ZH_HANS_TRANSLATIONS: Readonly> = { 'Show mouth': '显示嘴巴', 'Show nose': '显示鼻子', 'Simplified Chinese': '简体中文', + 'French': '法语', 'Smile': '微笑', 'Sleepy': '困倦', 'Softness': '柔和度', @@ -759,14 +761,769 @@ const ZH_HANS_TRANSLATIONS: Readonly> = { 'Width': '宽度' } +const FR_TRANSLATIONS: Readonly> = { + 'Angle': 'Angle', + 'Animation editor': 'Éditeur d\'animation', + 'Animation group': 'Groupe d\'animation', + 'Animation mode': 'Mode animation', + 'Animation name': 'Nom de l\'animation', + 'Back to animation library': 'Retour à la bibliothèque d\'animations', + 'Capture keyframe': 'Capturer l\'image clé', + 'Capture poses as keyframes': 'Capturer les poses comme images clés', + 'Capturing…': 'Capture…', + 'Custom animation': 'Animation personnalisée', + 'Custom animation editor': 'Éditeur d\'animation personnalisée', + 'Delete keyframe': 'Supprimer l\'image clé', + 'Delete saved animation': 'Supprimer l\'animation enregistrée', + 'Keyframe': 'Image clé', + 'Keyframes': 'Images clés', + 'Lock start position': 'Verrouiller la position de départ', + 'minimum': 'minimum', + 'Move the avatar, then capture the first keyframe.': 'Déplacez l\'avatar, puis capturez la première image clé.', + 'Playback settings': 'Paramètres de lecture', + 'Play once': 'Lire une fois', + 'Preset animations': 'Animations prédéfinies', + 'Preview': 'Aperçu', + 'Preview animation': 'Aperçu de l\'animation', + 'Save animation': 'Enregistrer l\'animation', + 'Saved animations': 'Animations enregistrées', + 'Select keyframe': 'Sélectionner une image clé', + 'Selected keyframe settings': 'Paramètres de l\'image clé sélectionnée', + 'Start frame': 'Image de départ', + 'Stop': 'Arrêter', + 'Stop preview': 'Arrêter l\'aperçu', + 'Add decal': 'Ajouter un décalque', + 'Apply palette to all parts?': 'Appliquer la palette à toutes les parties ?', + 'Apply to all': 'Appliquer à tout', + 'Appearance': 'Apparence', + 'Avatar type': 'Type d\'avatar', + 'Avatar parameters': 'Paramètres de l\'avatar', + 'Avatar templates': 'Modèles d\'avatar', + 'Avatar controls': 'Contrôles de l\'avatar', + 'Avatar outline': 'Contour de l\'avatar', + 'Avatar settings': 'Paramètres de l\'avatar', + 'Back to website': 'Retour au site', + 'Back to Build overview': 'Retour à l\'aperçu Construction', + 'Back to Effects overview': 'Retour à l\'aperçu Effets', + 'Back to Style overview': 'Retour à l\'aperçu Style', + 'Avatar shadow': 'Ombre de l\'avatar', + 'Background': 'Arrière-plan', + 'Big and small dots': 'Grands et petits points', + 'Base': 'Base', + 'Back': 'Arrière', + 'Body': 'Corps', + 'Body bottom taper': 'Rétrécissement inférieur du corps', + 'Bottom taper': 'Rétrécissement inférieur', + 'Bring your avatar to life': 'Donnez vie à votre avatar', + 'Build': 'Construction', + 'Bun': 'Chignon', + 'Camera background': 'Arrière-plan de la caméra', + 'Camera frame': 'Cadre de la caméra', + 'Cat ear height': 'Hauteur des oreilles de chat', + 'Cat ear size': 'Taille des oreilles de chat', + 'Cat ear width': 'Largeur des oreilles de chat', + 'Cat types': 'Types de chats', + 'Cancel': 'Annuler', + 'Close workspace sidebar': 'Fermer la barre latérale', + 'Current project': 'Projet actuel', + 'Editor': 'Éditeur', + 'Editor modes': 'Modes d\'édition', + 'Enable this effect to adjust its settings.': 'Activez cet effet pour ajuster ses paramètres.', + 'carousel': 'carrousel', + 'Cat': 'Chat', + 'Bear': 'Ours', + 'Bear ear height': 'Hauteur des oreilles d\'ours', + 'Bear ear size': 'Taille des oreilles d\'ours', + 'Bear ear width': 'Largeur des oreilles d\'ours', + 'Bear head height': 'Hauteur de tête d\'ours', + 'Bear head size': 'Taille de tête d\'ours', + 'Bear head width': 'Largeur de tête d\'ours', + 'Bear types': 'Types d\'ours', + 'Brown Bear': 'Ours brun', + 'Polar Bear': 'Ours polaire', + 'Asian Black Bear': 'Ours noir d\'Asie', + 'Giant Panda': 'Panda géant', + 'Spectacled Bear': 'Ours à lunettes', + 'Sun Bear': 'Ours malais', + 'Red Panda': 'Panda roux', + 'Koala': 'Koala', + 'Raccoon': 'Raton laveur', + 'Wombat': 'Wombat', + 'Teddy Bear': 'Ours en peluche', + 'Fox types': 'Types de renards', + 'Fox ear size': 'Taille des oreilles de renard', + 'Fox head size': 'Taille de tête de renard', + 'Fox ear width': 'Largeur des oreilles de renard', + 'Fox ear height': 'Hauteur des oreilles de renard', + 'Fox head width': 'Largeur de tête de renard', + 'Fox head height': 'Hauteur de tête de renard', + 'Red Fox': 'Renard roux', + 'Arctic Fox': 'Renard arctique', + 'Silver Fox': 'Renard argenté', + 'Fennec Fox': 'Fennec', + 'Hamster': 'Hamster', + 'Hamster types': 'Types de hamsters', + 'Hamster ear size': 'Taille des oreilles de hamster', + 'Hamster head size': 'Taille de tête de hamster', + 'Hamster ear width': 'Largeur des oreilles de hamster', + 'Hamster ear height': 'Hauteur des oreilles de hamster', + 'Hamster head width': 'Largeur de tête de hamster', + 'Hamster head height': 'Hauteur de tête de hamster', + 'Syrian Hamster': 'Hamster doré', + 'Pudding Hamster': 'Hamster pudding', + 'Silver Fox Hamster': 'Hamster renard argenté', + 'Sapphire Hamster': 'Hamster saphir', + 'Capybara': 'Capybara', + 'Capybara types': 'Types de capybaras', + 'Capybara ear size': 'Taille des oreilles de capybara', + 'Capybara head size': 'Taille de tête de capybara', + 'Capybara ear width': 'Largeur des oreilles de capybara', + 'Capybara ear height': 'Hauteur des oreilles de capybara', + 'Capybara head width': 'Largeur de tête de capybara', + 'Capybara head height': 'Hauteur de tête de capybara', + 'Sandy Capybara': 'Capybara sable', + 'Dark Capybara': 'Capybara foncé', + 'Capybara Pup': 'Bébé capybara', + 'Chick': 'Poussin', + 'Chick types': 'Types de poussins', + 'Chick head size': 'Taille de tête de poussin', + 'Chick head width': 'Largeur de tête de poussin', + 'Chick head height': 'Hauteur de tête de poussin', + 'Yellow Chick': 'Poussin jaune', + 'Silkie Chick': 'Poussin soie', + 'Barred Rock Chick': 'Poussin Plymouth Rock barré', + 'Buff Orpington Chick': 'Poussin Orpington fauve', + 'Beak size': 'Taille du bec', + 'Bill size': 'Taille du bec aplati', + 'Crest size': 'Taille de la huppe', + 'Tuft size': 'Taille de la touffe', + 'Duck': 'Canard', + 'Duck types': 'Types de canards', + 'Duck head size': 'Taille de tête de canard', + 'Duck head width': 'Largeur de tête de canard', + 'Duck head height': 'Hauteur de tête de canard', + 'Mallard Duck': 'Canard colvert', + 'Pekin Duck': 'Canard de Pékin', + 'Muscovy Duck': 'Canard de Barbarie', + 'Yellow Duckling': 'Caneton jaune', + 'Penguin': 'Pingouin', + 'Penguin types': 'Types de pingouins', + 'Penguin head size': 'Taille de tête de pingouin', + 'Penguin head width': 'Largeur de tête de pingouin', + 'Penguin head height': 'Hauteur de tête de pingouin', + 'Emperor Penguin': 'Pingouin empereur', + 'Adelie Penguin': 'Pingouin Adélie', + 'Gentoo Penguin': 'Pingouin papou', + 'Penguin Chick': 'Bébé pingouin', + 'Owl': 'Chouette', + 'Owl types': 'Types de chouettes', + 'Owl head size': 'Taille de tête de chouette', + 'Owl head width': 'Largeur de tête de chouette', + 'Owl head height': 'Hauteur de tête de chouette', + 'Barn Owl': 'Chouette effraie', + 'Snowy Owl': 'Harfang des neiges', + 'Great Horned Owl': 'Grand-duc d\'Amérique', + 'Little Owl': 'Chevêche d\'Athéna', + 'Parrot': 'Perroquet', + 'Parrot types': 'Types de perroquets', + 'Parrot head size': 'Taille de tête de perroquet', + 'Parrot head width': 'Largeur de tête de perroquet', + 'Parrot head height': 'Hauteur de tête de perroquet', + 'Scarlet Macaw': 'Ara rouge', + 'Blue-and-yellow Macaw': 'Ara bleu et jaune', + 'African Grey Parrot': 'Perroquet gris du Gabon', + 'Cockatiel': 'Calopsitte', + 'Goose': 'Oie', + 'Goose types': 'Types d\'oies', + 'Goose head size': 'Taille de tête d\'oie', + 'Goose head width': 'Largeur de tête d\'oie', + 'Goose head height': 'Hauteur de tête d\'oie', + 'Greylag Goose': 'Oie cendrée', + 'Canada Goose': 'Bernache du Canada', + 'Snow Goose': 'Oie des neiges', + 'White Gosling': 'Oison blanc', + 'Otter': 'Loutre', + 'Otter types': 'Types de loutres', + 'Otter ear size': 'Taille des oreilles de loutre', + 'Otter head size': 'Taille de tête de loutre', + 'Otter ear width': 'Largeur des oreilles de loutre', + 'Otter ear height': 'Hauteur des oreilles de loutre', + 'Otter head width': 'Largeur de tête de loutre', + 'Otter head height': 'Hauteur de tête de loutre', + 'Sea Otter': 'Loutre de mer', + 'River Otter': 'Loutre de rivière', + 'Asian Small-clawed Otter': 'Loutre cendrée', + 'Pig': 'Cochon', + 'Pig types': 'Types de cochons', + 'Pig ear size': 'Taille des oreilles de cochon', + 'Pig head size': 'Taille de tête de cochon', + 'Pig ear width': 'Largeur des oreilles de cochon', + 'Pig ear height': 'Hauteur des oreilles de cochon', + 'Pig head width': 'Largeur de tête de cochon', + 'Pig head height': 'Hauteur de tête de cochon', + 'Pink Pig': 'Cochon rose', + 'Black Pig': 'Cochon noir', + 'Spotted Pig': 'Cochon tacheté', + 'Wild Boar': 'Sanglier', + 'Deer': 'Cerf', + 'Deer types': 'Types de cerfs', + 'Deer ear size': 'Taille des oreilles de cerf', + 'Deer head size': 'Taille de tête de cerf', + 'Deer ear width': 'Largeur des oreilles de cerf', + 'Deer ear height': 'Hauteur des oreilles de cerf', + 'Deer head width': 'Largeur de tête de cerf', + 'Deer head height': 'Hauteur de tête de cerf', + 'Antler size': 'Taille des bois', + 'Sika Deer': 'Cerf sika', + 'Reindeer': 'Renne', + 'White Deer': 'Cerf blanc', + 'Deer Fawn': 'Faon', + 'Fawn': 'Faon', + 'Sheep': 'Mouton', + 'Sheep types': 'Types de moutons', + 'Sheep ear size': 'Taille des oreilles de mouton', + 'Sheep head size': 'Taille de tête de mouton', + 'Sheep ear width': 'Largeur des oreilles de mouton', + 'Sheep ear height': 'Hauteur des oreilles de mouton', + 'Sheep head width': 'Largeur de tête de mouton', + 'Sheep head height': 'Hauteur de tête de mouton', + 'Horn size': 'Taille des cornes', + 'White Sheep': 'Mouton blanc', + 'Black-faced Sheep': 'Mouton à face noire', + 'Horned Ram': 'Bélier à cornes', + 'Lamb': 'Agneau', + 'Mountain Goat': 'Chèvre de montagne', + 'Alpaca': 'Alpaga', + 'Alpaca types': 'Types d\'alpagas', + 'Alpaca ear size': 'Taille des oreilles d\'alpaga', + 'Alpaca head size': 'Taille de tête d\'alpaga', + 'Alpaca ear width': 'Largeur des oreilles d\'alpaga', + 'Alpaca ear height': 'Hauteur des oreilles d\'alpaga', + 'Alpaca head width': 'Largeur de tête d\'alpaga', + 'Alpaca head height': 'Hauteur de tête d\'alpaga', + 'Cream Alpaca': 'Alpaga crème', + 'Caramel Alpaca': 'Alpaga caramel', + 'Gray Alpaca': 'Alpaga gris', + 'Alpaca Cria': 'Bébé alpaga', + 'Cow': 'Vache', + 'Cow types': 'Types de vaches', + 'Cow ear size': 'Taille des oreilles de vache', + 'Cow head size': 'Taille de tête de vache', + 'Cow ear width': 'Largeur des oreilles de vache', + 'Cow ear height': 'Hauteur des oreilles de vache', + 'Cow head width': 'Largeur de tête de vache', + 'Cow horn size': 'Taille des cornes de vache', + 'Cow head height': 'Hauteur de tête de vache', + 'Dairy Cow': 'Vache laitière', + 'Jersey Cow': 'Vache Jersey', + 'Highland Cow': 'Vache des Highlands', + 'Cow Calf': 'Veau', + 'Squirrel': 'Écureuil', + 'Squirrel types': 'Types d\'écureuils', + 'Squirrel ear size': 'Taille des oreilles d\'écureuil', + 'Squirrel head size': 'Taille de tête d\'écureuil', + 'Squirrel ear width': 'Largeur des oreilles d\'écureuil', + 'Squirrel ear height': 'Hauteur des oreilles d\'écureuil', + 'Squirrel head width': 'Largeur de tête d\'écureuil', + 'Squirrel head height': 'Hauteur de tête d\'écureuil', + 'Squirrel tail size': 'Taille de la queue d\'écureuil', + 'Tail size': 'Taille de la queue', + 'Red Squirrel': 'Écureuil roux', + 'Gray Squirrel': 'Écureuil gris', + 'Chipmunk': 'Tamia', + 'Black Squirrel': 'Écureuil noir', + 'Tiger': 'Tigre', + 'Tiger types': 'Types de tigres', + 'Tiger ear size': 'Taille des oreilles de tigre', + 'Tiger head size': 'Taille de tête de tigre', + 'Tiger ear width': 'Largeur des oreilles de tigre', + 'Tiger ear height': 'Hauteur des oreilles de tigre', + 'Tiger head width': 'Largeur de tête de tigre', + 'Tiger head height': 'Hauteur de tête de tigre', + 'Bengal Tiger': 'Tigre du Bengale', + 'White Tiger': 'Tigre blanc', + 'Golden Tiger': 'Tigre doré', + 'Tiger Cub': 'Bébé tigre', + 'Lion': 'Lion', + 'Lion types': 'Types de lions', + 'Lion ear size': 'Taille des oreilles de lion', + 'Lion head size': 'Taille de tête de lion', + 'Lion ear width': 'Largeur des oreilles de lion', + 'Lion ear height': 'Hauteur des oreilles de lion', + 'Lion head width': 'Largeur de tête de lion', + 'Lion head height': 'Hauteur de tête de lion', + 'Lion mane size': 'Taille de la crinière du lion', + 'Mane size': 'Taille de la crinière', + 'African Lion': 'Lion d\'Afrique', + 'Lioness': 'Lionne', + 'White Lion': 'Lion blanc', + 'Lion Cub': 'Lionceau', + 'Hedgehog': 'Hérisson', + 'Hedgehog types': 'Types de hérissons', + 'Hedgehog ear size': 'Taille des oreilles de hérisson', + 'Hedgehog head size': 'Taille de tête de hérisson', + 'Hedgehog ear width': 'Largeur des oreilles de hérisson', + 'Hedgehog ear height': 'Hauteur des oreilles de hérisson', + 'Hedgehog head width': 'Largeur de tête de hérisson', + 'Hedgehog head height': 'Hauteur de tête de hérisson', + 'Hedgehog spine size': 'Taille des piquants de hérisson', + 'Spine size': 'Taille des piquants', + 'Incisor size': 'Taille des incisives', + 'European Hedgehog': 'Hérisson d\'Europe', + 'Cream Hedgehog': 'Hérisson crème', + 'Albino Hedgehog': 'Hérisson albinos', + 'Cinnamon Hedgehog': 'Hérisson cannelle', + 'Seal': 'Phoque', + 'Seal types': 'Types de phoques', + 'Seal head size': 'Taille de tête de phoque', + 'Seal head width': 'Largeur de tête de phoque', + 'Seal head height': 'Hauteur de tête de phoque', + 'Harbor Seal': 'Phoque commun', + 'Harp Seal': 'Phoque du Groenland', + 'Gray Seal': 'Phoque gris', + 'Seal Pup': 'Bébé phoque', + 'Beaver': 'Castor', + 'Beaver types': 'Types de castors', + 'Beaver ear size': 'Taille des oreilles de castor', + 'Beaver head size': 'Taille de tête de castor', + 'Beaver ear width': 'Largeur des oreilles de castor', + 'Beaver ear height': 'Hauteur des oreilles de castor', + 'Beaver head width': 'Largeur de tête de castor', + 'Beaver head height': 'Hauteur de tête de castor', + 'North American Beaver': 'Castor d\'Amérique', + 'Eurasian Beaver': 'Castor d\'Eurasie', + 'Dark Beaver': 'Castor foncé', + 'Beaver Kit': 'Bébé castor', + 'Guinea Pig': 'Cochon d\'Inde', + 'Guinea Pig types': 'Types de cochons d\'Inde', + 'Guinea Pig ear size': 'Taille des oreilles de cochon d\'Inde', + 'Guinea Pig head size': 'Taille de tête de cochon d\'Inde', + 'Guinea Pig ear width': 'Largeur des oreilles de cochon d\'Inde', + 'Guinea Pig ear height': 'Hauteur des oreilles de cochon d\'Inde', + 'Guinea Pig head width': 'Largeur de tête de cochon d\'Inde', + 'Guinea Pig head height': 'Hauteur de tête de cochon d\'Inde', + 'American Guinea Pig': 'Cochon d\'Inde américain', + 'Abyssinian Guinea Pig': 'Cochon d\'Inde abyssin', + 'Teddy Guinea Pig': 'Cochon d\'Inde teddy', + 'Guinea Pig Pup': 'Bébé cochon d\'Inde', + 'Chinchilla': 'Chinchilla', + 'Chinchilla types': 'Types de chinchillas', + 'Chinchilla ear size': 'Taille des oreilles de chinchilla', + 'Chinchilla head size': 'Taille de tête de chinchilla', + 'Chinchilla ear width': 'Largeur des oreilles de chinchilla', + 'Chinchilla ear height': 'Hauteur des oreilles de chinchilla', + 'Chinchilla head width': 'Largeur de tête de chinchilla', + 'Chinchilla head height': 'Hauteur de tête de chinchilla', + 'Gray Chinchilla': 'Chinchilla gris', + 'Beige Chinchilla': 'Chinchilla beige', + 'White Chinchilla': 'Chinchilla blanc', + 'Black Velvet Chinchilla': 'Chinchilla noir velours', + 'Ferret': 'Furet', + 'Ferret types': 'Types de furets', + 'Ferret ear size': 'Taille des oreilles de furet', + 'Ferret head size': 'Taille de tête de furet', + 'Ferret ear width': 'Largeur des oreilles de furet', + 'Ferret ear height': 'Hauteur des oreilles de furet', + 'Ferret head width': 'Largeur de tête de furet', + 'Ferret head height': 'Hauteur de tête de furet', + 'Sable Ferret': 'Furet zibeline', + 'Albino Ferret': 'Furet albinos', + 'Cinnamon Ferret': 'Furet cannelle', + 'Panda Ferret': 'Furet panda', + 'Monkey': 'Singe', + 'Monkey types': 'Types de singes', + 'Monkey ear size': 'Taille des oreilles de singe', + 'Monkey head size': 'Taille de tête de singe', + 'Monkey ear width': 'Largeur des oreilles de singe', + 'Monkey ear height': 'Hauteur des oreilles de singe', + 'Monkey head width': 'Largeur de tête de singe', + 'Monkey head height': 'Hauteur de tête de singe', + 'Macaque': 'Macaque', + 'Capuchin Monkey': 'Capucin', + 'Golden Monkey': 'Rhinopithèque doré', + 'Baby Monkey': 'Bébé singe', + 'Capsule': 'Capsule', + 'Circle': 'Cercle', + 'Close': 'Fermer', + 'Color': 'Couleur', + 'Cloud': 'Nuage', + 'Cone': 'Cône', + 'Cone roundness': 'Arrondi du cône', + 'Corner roundness': 'Arrondi des coins', + 'Copy SVG': 'Copier le SVG', + 'Create': 'Créer', + 'Curve': 'Courbe', + 'Current position': 'Position actuelle', + 'Current avatar': 'Avatar actuel', + 'Curvature': 'Courbure', + 'Cut direction': 'Sens de coupe', + 'Default face': 'Visage par défaut', + 'Delete decal': 'Supprimer le décalque', + 'Dark theme': 'Thème sombre', + 'Diamond': 'Losange', + 'Direction': 'Direction', + 'Distance': 'Distance', + 'Depth': 'Profondeur', + 'Part bottom taper': 'Rétrécissement inférieur de la partie', + 'Ear height': 'Hauteur des oreilles', + 'Ear width': 'Largeur des oreilles', + 'Download SVG': 'Télécharger le SVG', + 'Download PNG': 'Télécharger le PNG', + 'Download GIF': 'Télécharger le GIF', + 'Export avatar': 'Exporter l\'avatar', + 'Dog': 'Chien', + 'Dog ear height': 'Hauteur des oreilles de chien', + 'Dog ear size': 'Taille des oreilles de chien', + 'Dog ear width': 'Largeur des oreilles de chien', + 'Dog head height': 'Hauteur de tête de chien', + 'Dog head size': 'Taille de tête de chien', + 'Dog head width': 'Largeur de tête de chien', + 'Dog types': 'Types de chiens', + 'Shiba Inu': 'Shiba Inu', + 'Husky': 'Husky', + 'Head height': 'Hauteur de la tête', + 'Head width': 'Largeur de la tête', + 'Corgi': 'Corgi', + 'Golden Retriever': 'Golden Retriever', + 'Border Collie': 'Border Collie', + 'Dalmatian': 'Dalmatien', + 'Duration': 'Durée', + 'Editing': 'Édition', + 'Easing': 'Lissage', + 'Effects': 'Effets', + 'Entity parts': 'Parties de l\'entité', + 'Ellipse': 'Ellipse', + 'English': 'Anglais', + 'French': 'Français', + 'Exporting GIF…': 'Export du GIF…', + 'Enter camera mode': 'Passer en mode caméra', + 'Exit camera mode': 'Quitter le mode caméra', + 'Exit animation group editing': 'Quitter l\'édition du groupe d\'animations', + 'Eyes': 'Yeux', + 'Eye highlights': 'Reflets des yeux', + 'Eye highlight horizontal position': 'Position horizontale du reflet', + 'Eye highlight opacity': 'Opacité du reflet', + 'Eye highlight size': 'Taille du reflet', + 'Eye highlight vertical position': 'Position verticale du reflet', + 'Face': 'Visage', + 'Flat lines': 'Lignes plates', + 'Face mask': 'Masque facial', + 'Alert': 'Vigilant', + 'Bashful': 'Timide', + 'Calm': 'Calme', + 'Cool': 'Cool', + 'Cute': 'Mignon', + 'Dizzy': 'Étourdi', + 'Face parts': 'Éléments du visage', + 'Face presets': 'Visages prédéfinis', + 'Face shadow': 'Ombre du visage', + 'Follow Seed': 'Suivre la graine', + 'Focused': 'Concentré', + 'Fox': 'Renard', + 'Far': 'Loin', + 'Flat': 'Plat', + 'Frame shadow': 'Ombre du cadre', + 'Frame': 'Cadre', + 'First frame': 'Première image', + 'Fixed position': 'Position fixe', + 'Frustum': 'Tronc de cône', + 'Frown': 'Moue', + 'Gap': 'Écart', + 'Gradient': 'Dégradé', + 'Grid density': 'Densité de la grille', + 'Generate random Seed': 'Générer une graine aléatoire', + 'Height': 'Hauteur', + 'Happy': 'Joyeux', + 'Half cone': 'Demi-cône', + 'Highlight': 'Reflet', + 'Highlight color': 'Couleur du reflet', + 'Highlight opacity': 'Opacité du reflet', + 'Highlight position X': 'Position X du reflet', + 'Highlight position Y': 'Position Y du reflet', + 'Highlight size': 'Taille du reflet', + 'Hide controls': 'Masquer les contrôles', + 'Hide controls sidebar': 'Masquer la barre de contrôles', + 'Hide resources': 'Masquer les ressources', + 'Hide resources sidebar': 'Masquer la barre de ressources', + 'High': 'Élevé', + 'Hollow': 'Creux', + 'Language': 'Langue', + 'Length': 'Longueur', + 'Innocent': 'Innocent', + 'Light theme': 'Thème clair', + 'Light coat patch': 'Tache de poil clair', + 'Light coat patch shape': 'Forme de la tache claire', + 'Left tilt': 'Inclinaison gauche', + 'Less': 'Moins', + 'Light source': 'Source lumineuse', + 'Low': 'Faible', + 'Mouth': 'Bouche', + 'Mouth shape': 'Forme de la bouche', + 'Minimal': 'Minimal', + 'Mixed signal': 'Signal mixte', + 'More': 'Plus', + 'More panel actions': 'Plus d\'actions du panneau', + 'Panel actions': 'Actions du panneau', + 'More timeline options': 'Plus d\'options de la chronologie', + 'Timeline options': 'Options de la chronologie', + 'Reset playhead': 'Réinitialiser la tête de lecture', + 'Reset timeline zoom': 'Réinitialiser le zoom de la chronologie', + 'Collapse all loop groups': 'Réduire tous les groupes en boucle', + 'Expand all loop groups': 'Développer tous les groupes en boucle', + 'Auto replay': 'Relecture automatique', + 'Clear selected track': 'Effacer la piste sélectionnée', + 'Clear all clips': 'Effacer tous les clips', + 'More presets': 'Plus de préréglages', + 'Name': 'Nom', + 'Near': 'Proche', + 'Nose': 'Nez', + 'No presets found': 'Aucun préréglage trouvé', + 'Opacity': 'Opacité', + 'Off': 'Désactivé', + 'Open animation editor': 'Ouvrir l\'éditeur d\'animation', + 'Open Avatar on GitHub': 'Ouvrir Avatar sur GitHub', + 'Open project menu': 'Ouvrir le menu du projet', + 'Open workspace sidebar': 'Ouvrir la barre latérale', + 'Project': 'Projet', + 'Project menu': 'Menu du projet', + 'Project name': 'Nom du projet', + 'Recent projects': 'Projets récents', + 'No recent projects': 'Aucun projet récent', + 'Rename project': 'Renommer le projet', + 'Delete project': 'Supprimer le projet', + 'Advanced options': 'Options avancées', + 'Resize avatar controls': 'Redimensionner les contrôles de l\'avatar', + 'Resize avatar resources': 'Redimensionner les ressources de l\'avatar', + 'Save as': 'Enregistrer sous', + 'Save failed': 'Échec de l\'enregistrement', + 'Save project': 'Enregistrer le projet', + 'Saved': 'Enregistré', + 'Saved projects will appear here.': 'Les projets enregistrés apparaîtront ici.', + 'Saving…': 'Enregistrement…', + 'Show resources': 'Afficher les ressources', + 'Show resources sidebar': 'Afficher la barre de ressources', + 'Show controls': 'Afficher les contrôles', + 'Show controls sidebar': 'Afficher la barre de contrôles', + 'OneWorks Avatar home': 'Accueil OneWorks Avatar', + 'Opening editor': 'Ouverture de l\'éditeur', + 'Palette': 'Palette', + 'Palette presets': 'Palettes prédéfinies', + 'Pixel color count': 'Nombre de couleurs de pixels', + 'Pixel dithering': 'Tramage des pixels', + 'Pixel sampling': 'Échantillonnage des pixels', + 'Pixel size': 'Taille des pixels', + 'Pixel style': 'Style pixel', + 'Part': 'Partie', + 'Part position X': 'Position X de la partie', + 'Part position Y': 'Position Y de la partie', + 'Part position Z': 'Position Z de la partie', + 'Part width': 'Largeur de la partie', + 'Part height': 'Hauteur de la partie', + 'Part depth': 'Profondeur de la partie', + 'Part rotation X': 'Rotation X de la partie', + 'Part rotation Y': 'Rotation Y de la partie', + 'Part rotation Z': 'Rotation Z de la partie', + 'Part cone roundness': 'Arrondi du cône de la partie', + 'Part cut direction': 'Sens de coupe de la partie', + 'Part corner roundness': 'Arrondi des coins de la partie', + 'Part material': 'Matériau de la partie', + 'Playful': 'Enjoué', + 'Peek': 'Coup d\'œil', + 'Position Y': 'Position Y', + 'Vertical position': 'Position verticale', + 'View composition': 'Composition de la vue', + 'Position': 'Position', + 'Position X': 'Position X', + 'Position Z': 'Position Z', + 'Rabbit': 'Lapin', + 'Rabbit ear height': 'Hauteur des oreilles de lapin', + 'Rabbit ear size': 'Taille des oreilles de lapin', + 'Rabbit ear width': 'Largeur des oreilles de lapin', + 'Rabbit head height': 'Hauteur de tête de lapin', + 'Rabbit head size': 'Taille de tête de lapin', + 'Rabbit head width': 'Largeur de tête de lapin', + 'Rabbit types': 'Types de lapins', + 'Holland Lop': 'Lapin bélier nain', + 'Netherland Dwarf': 'Lapin nain des Pays-Bas', + 'Dutch Rabbit': 'Lapin hollandais', + 'Himalayan Rabbit': 'Lapin himalayen', + 'Lionhead Rabbit': 'Lapin tête de lion', + 'English Spot': 'Lapin papillon anglais', + 'Radial pleats': 'Plis radiaux', + 'Playback': 'Lecture', + 'Loop': 'Boucle', + 'Once': 'Une fois', + 'Reset default face': 'Réinitialiser le visage par défaut', + 'Right tilt': 'Inclinaison droite', + 'Retry GIF export': 'Réessayer l\'export GIF', + 'Rotation': 'Rotation', + 'Rotation (overall)': 'Rotation (globale)', + 'Rotation X': 'Rotation X', + 'Rotation Y': 'Rotation Y', + 'Rotation Z': 'Rotation Z', + 'Rounded': 'Arrondi', + 'Rounded triangle': 'Triangle arrondi', + 'Side eye': 'Regard de côté', + 'Soft': 'Doux', + 'Sparkle': 'Étincelant', + 'Chevron': 'Chevron', + 'Scrunch': 'Froncé', + 'Squint': 'Yeux plissés', + 'Tiny': 'Minuscule', + 'Wink': 'Clin d\'œil', + 'Rounded square': 'Carré arrondi', + 'Rounded trapezoid': 'Trapèze arrondi', + 'Roundness': 'Arrondi', + 'Save a look to build your history.': 'Enregistrez un look pour construire votre historique.', + 'Save current preset': 'Enregistrer le préréglage actuel', + 'Saved presets': 'Préréglages enregistrés', + 'Saved preset': 'Préréglage enregistré', + 'Saved looks': 'Looks enregistrés', + 'Seed': 'Graine', + 'Seed settings': 'Paramètres de la graine', + 'Seeded fields': 'Champs liés', + 'Only linked fields change when the Seed changes.': 'Seuls les champs liés changent lorsque la graine change.', + 'Search presets': 'Rechercher des préréglages', + 'Select avatar': 'Choisir un avatar', + 'Select language': 'Choisir la langue', + 'Selected avatar': 'Avatar sélectionné', + 'Select an animation first': 'Choisissez d\'abord une animation', + 'Serious': 'Sérieux', + 'Show mouth': 'Afficher la bouche', + 'Show nose': 'Afficher le nez', + 'Simplified Chinese': 'Chinois simplifié', + 'Smile': 'Sourire', + 'Sleepy': 'Endormi', + 'Softness': 'Douceur', + 'Solid': 'Uni', + 'Sampling': 'Échantillonnage', + 'Shape': 'Forme', + 'Shape parts': 'Éléments de forme', + 'Shape type': 'Type de forme', + 'Shape name': 'Nom de la forme', + 'Shape node tree': 'Arborescence des formes', + 'Shadows': 'Ombres', + 'Avatar root': 'Racine de l\'avatar', + 'Group': 'Groupe', + 'Group name': 'Nom du groupe', + 'Group selection': 'Sélection du groupe', + 'Group motion': 'Mouvement du groupe', + 'Collapse group': 'Réduire le groupe', + 'Expand group': 'Développer le groupe', + 'New shape': 'Nouvelle forme', + 'New group': 'Nouveau groupe', + 'Duplicate shape': 'Dupliquer la forme', + 'Delete shape': 'Supprimer la forme', + 'Remove group': 'Supprimer le groupe', + 'Node actions': 'Actions du nœud', + 'Right click to add a shape or group': 'Clic droit pour ajouter une forme ou un groupe', + 'Copy': 'Copie', + 'shapes': 'formes', + 'Offset X': 'Décalage X', + 'Offset Y': 'Décalage Y', + 'Offset Z': 'Décalage Z', + 'Group offset X': 'Décalage X du groupe', + 'Group offset Y': 'Décalage Y du groupe', + 'Group offset Z': 'Décalage Z du groupe', + 'Add a shape to configure this group.': 'Ajoutez une forme pour configurer ce groupe.', + 'Center': 'Centre', + 'Dominant': 'Dominante', + 'Median': 'Médiane', + 'SLIC': 'SLIC', + 'Colors': 'Couleurs', + 'Dithering': 'Tramage', + 'Ordered': 'Ordonné', + 'Sphere': 'Sphère', + 'Square': 'Carré', + 'Start creating': 'Commencer à créer', + 'Edit': 'Modifier', + 'Surprise me': 'Surprends-moi', + 'View more': 'Voir plus', + 'All avatars': 'Tous les avatars', + 'Didn\'t find your avatar?': 'Vous n\'avez pas trouvé votre avatar ?', + 'Create your own avatar from the model you selected.': 'Créez votre propre avatar à partir du modèle sélectionné.', + 'Create your own': 'Créez le vôtre', + 'Style': 'Style', + 'Sun': 'Soleil', + 'Surprised': 'Surpris', + 'Siamese': 'Siamois', + 'British Shorthair': 'British Shorthair', + 'Russian Blue': 'Bleu russe', + 'Orange Tabby': 'Tabby roux', + 'Cow Cat': 'Chat vache', + 'Black Cat': 'Chat noir', + 'Teardrop': 'Goutte', + 'This entity uses multiple materials. Applying this palette will replace all part colors.': + 'Cette entité utilise plusieurs matériaux. Appliquer cette palette remplacera toutes les couleurs des parties.', + 'Switch to dark theme': 'Passer au thème sombre', + 'Switch to light theme': 'Passer au thème clair', + 'Untitled project': 'Projet sans titre', + 'Workspace': 'Espace de travail', + 'Switch to': 'Passer à', + 'Transparent': 'Transparent', + 'Surface grid density': 'Densité de grille de surface', + 'Surface decal': 'Décalque de surface', + 'Surface decal shape': 'Forme du décalque de surface', + 'Tapered band': 'Bande effilée', + 'Bend': 'Courbure', + 'Decal bend': 'Courbure du décalque', + 'Surface decals': 'Décalques de surface', + 'Coat pattern': 'Motif de pelage', + 'Pattern algorithm': 'Algorithme de motif', + 'Pattern layout': 'Disposition du motif', + 'Random': 'Aléatoire', + 'Mackerel': 'Maquereau', + 'Classic': 'Classique', + 'Broken': 'Brisé', + 'Spotted': 'Tacheté', + 'Density': 'Densité', + 'Jitter': 'Gigue', + 'Symmetry': 'Symétrie', + 'Contrast': 'Contraste', + 'Breakup': 'Fragmentation', + 'Convert to editable decals': 'Convertir en décalques modifiables', + 'Tabby pattern': 'Motif tabby', + 'Tabby coat patch': 'Tache de pelage tabby', + 'Tabby spot': 'Tache tabby', + 'Tabby stripe': 'Rayure tabby', + 'Surface side': 'Face de la surface', + 'Face plane': 'Plan du visage', + 'Current Seed': 'Graine actuelle', + 'Decal height': 'Hauteur du décalque', + 'Decal opacity': 'Opacité du décalque', + 'Decal position X': 'Position X du décalque', + 'Decal position Y': 'Position Y du décalque', + 'Decal rotation': 'Rotation du décalque', + 'Decal width': 'Largeur du décalque', + 'Target part': 'Partie cible', + 'Front': 'Avant', + 'Left': 'Gauche', + 'Right': 'Droite', + 'SVG copied': 'SVG copié', + 'Size': 'Taille', + 'Thickness': 'Épaisseur', + 'Triangle': 'Triangle', + 'Untitled animation': 'Animation sans titre', + 'Width': 'Largeur' +} + export const normalizeAvatarLocale = (value: string | null | undefined): AvatarLocale | null => { if (value === 'en') return 'en' if (value === 'zh-Hans' || value === 'zh-CN' || value?.toLowerCase().startsWith('zh')) return 'zh-Hans' + if (value === 'fr' || value?.toLowerCase().startsWith('fr')) return 'fr' return null } export const translateAvatarText = (locale: AvatarLocale, text: string) => { - return locale === 'zh-Hans' ? ZH_HANS_TRANSLATIONS[text] ?? text : text + if (locale === 'zh-Hans') return ZH_HANS_TRANSLATIONS[text] ?? text + if (locale === 'fr') return FR_TRANSLATIONS[text] ?? text + return text +} + +export const hasAvatarTranslation = (locale: AvatarLocale, text: string) => { + if (locale === 'zh-Hans') return Object.prototype.hasOwnProperty.call(ZH_HANS_TRANSLATIONS, text) + if (locale === 'fr') return Object.prototype.hasOwnProperty.call(FR_TRANSLATIONS, text) + return false } const getInitialAvatarLocale = (): AvatarLocale => {