Accents and bug tracking

This commit is contained in:
Samuel Bouchet 2026-03-11 18:33:10 +01:00
parent c9f8a9566a
commit 2c96cba174
10 changed files with 537 additions and 240 deletions

73
bugs.md Normal file
View file

@ -0,0 +1,73 @@
# Bug tracker
Les sujets dans FIXME doivent être corrigé, puis déplacé dans "DONE", puis commit de ce fichier avec le fix.
# FIXME
## Adventure error
```
Que veux-tu faire ?
1. Ouvrir une boite (1)
2. Voir l'inventaire
3. Partir a l'aventure
4. Sauvegarder
5. Retourner au menu
>
Entre un nombre entre 1 et 5.
> 3
Partir a l'aventure
1. DarkFantasy
2. Retour
> 1
ERROR: Adventure error: Unexpected character: ? at (124:81:6496:0)
Appuie sur une touche pour continuer...
```
=> Les aventures devraient toutes fonctionner
=> écrire des tests pour valider a priori que ce genre de bug ne survient plus
## Arms translation
```
Que veux-tu faire ?
1. Ouvrir une boite (1)
2. Voir l'inventaire
3. Partir a l'aventure
4. Changer d'apparence
5. Sauvegarder
6. Retourner au menu
> 4
Changer d'apparence
1. [Arms] Bras normaux
2. Retour
> 1
Equipped Arms: Regular
```
=> "Arms" devrait être traduit en français
=> "Equipped Arms: Regular" devrait être traduit en français
=> Vérifie dans le code qu'il n'y a pas d'autres textes en dur
## Duplicated cosmetics
```
Que veux-tu faire ?
1. Ouvrir une boite (2)
2. Voir l'inventaire
3. Partir a l'aventure
4. Changer d'apparence
5. Sauvegarder
6. Retourner au menu
> 4
Changer d'apparence
1. [Arms] Bras normaux
2. [Hair] Cheveux longs
3. [Arms] Bras mecaniques
4. [Hair] Cheveux courts
5. [Body] T-shirt basique
6. [Eyes] Yeux verts
7. Retour
```
Bien que l'inventaire ne m'indique qu'un seul exemplaire possédé,
observé: le choix multiple m'indique plusieurs fois les même cosmétiques.
Attendu: le choix multiple n'indique qu'une fois chaque cosmétique.
# DONE

View file

@ -208,6 +208,7 @@
"rollCount": 1, "rollCount": 1,
"entries": [ "entries": [
{"itemDefinitionId": "meta_colors", "weight": 5}, {"itemDefinitionId": "meta_colors", "weight": 5},
{"itemDefinitionId": "meta_autosave", "weight": 6},
{"itemDefinitionId": "meta_arrows", "weight": 4}, {"itemDefinitionId": "meta_arrows", "weight": 4},
{"itemDefinitionId": "meta_animation", "weight": 4}, {"itemDefinitionId": "meta_animation", "weight": 4},
{"itemDefinitionId": "box_meta_interface", "weight": 1} {"itemDefinitionId": "box_meta_interface", "weight": 1}

View file

@ -11,6 +11,7 @@
{"id": "meta_shortcuts", "nameKey": "meta.shortcuts", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "KeyboardShortcuts"}, {"id": "meta_shortcuts", "nameKey": "meta.shortcuts", "category": "Meta", "rarity": "Rare", "tags": ["Meta"], "metaUnlock": "KeyboardShortcuts"},
{"id": "meta_animation", "nameKey": "meta.animation", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "BoxAnimation"}, {"id": "meta_animation", "nameKey": "meta.animation", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "BoxAnimation"},
{"id": "meta_crafting", "nameKey": "meta.crafting", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "CraftingPanel"}, {"id": "meta_crafting", "nameKey": "meta.crafting", "category": "Meta", "rarity": "Epic", "tags": ["Meta"], "metaUnlock": "CraftingPanel"},
{"id": "meta_autosave", "nameKey": "meta.autosave", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta"], "metaUnlock": "AutoSave"},
{"id": "meta_resource_health", "nameKey": "resource.health", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Health"}, {"id": "meta_resource_health", "nameKey": "resource.health", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Health"},
{"id": "meta_resource_mana", "nameKey": "resource.mana", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Mana"}, {"id": "meta_resource_mana", "nameKey": "resource.mana", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Mana"},
{"id": "meta_resource_food", "nameKey": "resource.food", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Food"}, {"id": "meta_resource_food", "nameKey": "resource.food", "category": "Meta", "rarity": "Uncommon", "tags": ["Meta", "ResourceVisibility"], "resourceType": "Food"},

View file

@ -425,5 +425,7 @@
"box.endgame.desc": "You found all the resources. This is it. The last box. Are you ready?", "box.endgame.desc": "You found all the resources. This is it. The last box. Are you ready?",
"item.endgame_crown": "Crown of Completion", "item.endgame_crown": "Crown of Completion",
"item.destiny_token": "Token of Destiny", "item.destiny_token": "Token of Destiny",
"adventure.secret_branch_found": "You feel a hidden path revealing itself..." "adventure.secret_branch_found": "You feel a hidden path revealing itself...",
"meta.autosave": "Auto-Save",
"save.autosaved": "Game auto-saved."
} }

View file

@ -1,5 +1,5 @@
{ {
"game.title": "OUVRE LA BOITE", "game.title": "OUVRE LA BOÎTE",
"game.subtitle": "Qu'est-ce qu'il y a dedans ? Un seul moyen de le savoir.", "game.subtitle": "Qu'est-ce qu'il y a dedans ? Un seul moyen de le savoir.",
"game.version": "v0.1.0", "game.version": "v0.1.0",
@ -10,139 +10,139 @@
"menu.back": "Retour", "menu.back": "Retour",
"menu.continue": "Continuer", "menu.continue": "Continuer",
"menu.save": "Sauvegarder", "menu.save": "Sauvegarder",
"menu.settings": "Parametres", "menu.settings": "Paramètres",
"action.open_box": "Ouvrir une boite", "action.open_box": "Ouvrir une boîte",
"action.inventory": "Voir l'inventaire", "action.inventory": "Voir l'inventaire",
"action.craft": "Fabriquer", "action.craft": "Fabriquer",
"action.adventure": "Partir a l'aventure", "action.adventure": "Partir à l'aventure",
"action.appearance": "Changer d'apparence", "action.appearance": "Changer d'apparence",
"action.save": "Sauvegarder", "action.save": "Sauvegarder",
"action.quit": "Retourner au menu", "action.quit": "Retourner au menu",
"prompt.name": "Quel est ton nom, brave ouvreur de boites ?", "prompt.name": "Quel est ton nom, brave ouvreur de boîtes ?",
"prompt.choose_action": "Que veux-tu faire ?", "prompt.choose_action": "Que veux-tu faire ?",
"prompt.choose_box": "Quelle boite veux-tu ouvrir ?", "prompt.choose_box": "Quelle boîte veux-tu ouvrir ?",
"prompt.choose_interaction": "Plusieurs interactions possibles ! Choisis-en une :", "prompt.choose_interaction": "Plusieurs interactions possibles ! Choisis-en une :",
"prompt.press_key": "Appuie sur une touche pour continuer...", "prompt.press_key": "Appuie sur une touche pour continuer...",
"box.opening": "Ouverture de {0}...", "box.opening": "Ouverture de {0}...",
"box.opened": "{0} ouverte ! (Rarete : {1})", "box.opened": "{0} ouverte ! (Rareté : {1})",
"box.opened_short": "{0} ouverte !", "box.opened_short": "{0} ouverte !",
"box.shimmer": "Quelque chose scintille...", "box.shimmer": "Quelque chose scintille...",
"box.found": "Tu as trouve : {0} !", "box.found": "Tu as trouvé : {0} !",
"box.found_box": "A l'interieur il y avait... une autre boite ! {0} !", "box.found_box": "À l'intérieur il y avait... une autre boîte ! {0} !",
"box.empty": "La boite est vide ! Philosophique.", "box.empty": "La boîte est vide ! Philosophique.",
"box.no_boxes": "Tu n'as aucune boite. Comment t'as fait ?", "box.no_boxes": "Tu n'as aucune boîte. Comment t'as fait ?",
"box.auto_open": "{0} s'ouvre automatiquement !", "box.auto_open": "{0} s'ouvre automatiquement !",
"loot.received": "Tu as recu :", "loot.received": "Tu as reçu :",
"loot.title": "Butin !", "loot.title": "Butin !",
"loot.name": "Nom", "loot.name": "Nom",
"loot.rarity": "Rarete", "loot.rarity": "Rareté",
"loot.category": "Categorie", "loot.category": "Catégorie",
"ui.feature_unlocked": "NOUVELLE FONCTIONNALITE : {0}", "ui.feature_unlocked": "NOUVELLE FONCTIONNALITÉ : {0}",
"ui.completion": "Completion : {0}%", "ui.completion": "Complétion : {0}%",
"prompt.what_do": "Que fais-tu ?", "prompt.what_do": "Que fais-tu ?",
"prompt.invalid_choice": "Entre un nombre entre 1 et {0}.", "prompt.invalid_choice": "Entre un nombre entre 1 et {0}.",
"box.starter": "Boite de depart", "box.starter": "Boîte de départ",
"box.starter.desc": "Ta premiere boite. Le debut de tout. Ou de rien. Probablement de quelque chose quand meme.", "box.starter.desc": "Ta première boîte. Le début de tout. Ou de rien. Probablement de quelque chose quand même.",
"box.box_of_boxes": "Boite a boite", "box.box_of_boxes": "Boîte à boîte",
"box.box_of_boxes.desc": "Une boite qui contient... des boites. C'est des boites jusqu'en bas.", "box.box_of_boxes.desc": "Une boîte qui contient... des boîtes. C'est des boîtes jusqu'en bas.",
"box.not_great": "Boite pas ouf", "box.not_great": "Boîte pas ouf",
"box.not_great.desc": "Elle est pas geniale. Elle est pas terrible. Elle... est.", "box.not_great.desc": "Elle est pas géniale. Elle est pas terrible. Elle... est.",
"box.ok_tier": "Boite ok tiers", "box.ok_tier": "Boîte ok tiers",
"box.ok_tier.desc": "La mediocrite n'a jamais ete aussi carree.", "box.ok_tier.desc": "La médiocrité n'a jamais été aussi carrée.",
"box.cool": "Boite coolos", "box.cool": "Boîte coolos",
"box.cool.desc": "La on commence a causer. A causer cool.", "box.cool.desc": "Là on commence à causer. À causer cool.",
"box.epic": "Boite epique", "box.epic": "Boîte épique",
"box.epic.desc": "L'orchestre s'intensifie. La foule retient son souffle. C'est... une boite.", "box.epic.desc": "L'orchestre s'intensifie. La foule retient son souffle. C'est... une boîte.",
"box.legendhair": "Boite legend'hair", "box.legendhair": "Boîte legend'hair",
"box.legendhair.desc": "La coiffure la plus legendaire que tu deballeras jamais. Cheveu-jour-d'hui, legende demain.", "box.legendhair.desc": "La coiffure la plus légendaire que tu déballeras jamais. Cheveu-jour-d'hui, légende demain.",
"box.legendary": "Boite legendaire", "box.legendary": "Boîte légendaire",
"box.legendary.desc": "Les legendes parlent de cette boite. Doucement quand meme, c'est une boite.", "box.legendary.desc": "Les légendes parlent de cette boîte. Doucement quand même, c'est une boîte.",
"box.adventure": "Boite aventure", "box.adventure": "Boîte aventure",
"box.adventure.desc": "Contient la cle de l'aventure ! Litteralement, parfois.", "box.adventure.desc": "Contient la clé de l'aventure ! Littéralement, parfois.",
"box.style": "Boite stylee", "box.style": "Boîte stylée",
"box.style.desc": "La mode est ephemere. Le style sorti d'une boite est eternel.", "box.style.desc": "La mode est éphémère. Le style sorti d'une boîte est éternel.",
"box.improvement": "Boite d'amelioration", "box.improvement": "Boîte d'amélioration",
"box.improvement.desc": "On peut toujours s'ameliorer. Surtout avec des boites.", "box.improvement.desc": "On peut toujours s'améliorer. Surtout avec des boîtes.",
"box.supply": "Boite de fourniture", "box.supply": "Boîte de fourniture",
"box.supply.desc": "Des fournitures ! Le sang vital de tout passione d'ouverture de boites.", "box.supply.desc": "Des fournitures ! Le sang vital de tout passionné d'ouverture de boîtes.",
"box.meta_basics": "Boite Meta - Les Bases", "box.meta_basics": "Boîte Méta - Les Bases",
"box.meta_basics.desc": "Couleurs, fleches, animations. Le fondement de la vision.", "box.meta_basics.desc": "Couleurs, flèches, animations. Le fondement de la vision.",
"box.meta_interface": "Boite Meta - L'Interface", "box.meta_interface": "Boîte Méta - L'Interface",
"box.meta_interface.desc": "Panneaux, ressources, stats. Les outils de la comprehension.", "box.meta_interface.desc": "Panneaux, ressources, stats. Les outils de la compréhension.",
"box.meta_deep": "Boite Meta - Personnalisation", "box.meta_deep": "Boîte Méta - Personnalisation",
"box.meta_deep.desc": "Couleurs etendues, artisanat, chat, portrait. Exprime-toi.", "box.meta_deep.desc": "Couleurs étendues, artisanat, chat, portrait. Exprime-toi.",
"box.meta_resources": "Boite Meta - Ressources", "box.meta_resources": "Boîte Méta - Ressources",
"box.meta_resources.desc": "Deverouille la capacite de voir ce que tu as. Et ce qui te manque.", "box.meta_resources.desc": "Déverrouille la capacité de voir ce que tu as. Et ce qui te manque.",
"box.meta_mastery": "Boite Meta - La Maitrise", "box.meta_mastery": "Boîte Méta - La Maîtrise",
"box.meta_mastery.desc": "Mise en page, stats, polices. Les touches finales d'un vrai maitre des boites.", "box.meta_mastery.desc": "Mise en page, stats, polices. Les touches finales d'un vrai maître des boîtes.",
"box.black": "Boite noire", "box.black": "Boîte noire",
"box.black.desc": "Personne ne sait ce qu'il y a dedans. Meme pas la boite.", "box.black.desc": "Personne ne sait ce qu'il y a dedans. Même pas la boîte.",
"box.story": "Boite a histoire", "box.story": "Boîte à histoire",
"box.story.desc": "Chaque boite a une histoire. Celle-ci plus que les autres.", "box.story.desc": "Chaque boîte a une histoire. Celle-ci plus que les autres.",
"box.music": "Boite a musique", "box.music": "Boîte à musique",
"box.music.desc": "Do do do do. La musique de boite c'est la meilleure musique.", "box.music.desc": "Do do do do. La musique de boîte c'est la meilleure musique.",
"box.cookie": "Boite a Cookies", "box.cookie": "Boîte à Cookies",
"box.cookie.desc": "La fortune sourit aux audacieux. Et a ceux qui ouvrent des boites.", "box.cookie.desc": "La fortune sourit aux audacieux. Et à ceux qui ouvrent des boîtes.",
"box.adventure.space": "Boite d'aventure spatiale", "box.adventure.space": "Boîte d'aventure spatiale",
"box.adventure.space.desc": "Vers l'infini et au-dela ! (Boite non incluse dans l'infini)", "box.adventure.space.desc": "Vers l'infini et au-delà ! (Boîte non incluse dans l'infini)",
"box.adventure.medieval": "Boite d'aventure medievale", "box.adventure.medieval": "Boîte d'aventure médiévale",
"box.adventure.medieval.desc": "Oyez, oyez ! Une boite d'aventure d'antan !", "box.adventure.medieval.desc": "Oyez, oyez ! Une boîte d'aventure d'antan !",
"box.adventure.pirate": "Boite d'aventure pirate", "box.adventure.pirate": "Boîte d'aventure pirate",
"box.adventure.pirate.desc": "Arr ! X marque la boite !", "box.adventure.pirate.desc": "Arr ! X marque la boîte !",
"box.adventure.contemporary": "Boite d'aventure contemporaine", "box.adventure.contemporary": "Boîte d'aventure contemporaine",
"box.adventure.contemporary.desc": "Une boite pour les temps modernes. Livree avec l'anxiete du WiFi.", "box.adventure.contemporary.desc": "Une boîte pour les temps modernes. Livrée avec l'anxiété du WiFi.",
"box.adventure.sentimental": "Boite d'aventure sentimentale", "box.adventure.sentimental": "Boîte d'aventure sentimentale",
"box.adventure.sentimental.desc": "Cette boite te fait ressentir des choses. Surtout de la curiosite.", "box.adventure.sentimental.desc": "Cette boîte te fait ressentir des choses. Surtout de la curiosité.",
"box.adventure.prehistoric": "Boite d'aventure prehistorique", "box.adventure.prehistoric": "Boîte d'aventure préhistorique",
"box.adventure.prehistoric.desc": "Ouga bouga boite. Tres vieille. Beaucoup mystere.", "box.adventure.prehistoric.desc": "Ouga bouga boîte. Très vieille. Beaucoup mystère.",
"box.adventure.cosmic": "Boite d'aventure cosmique", "box.adventure.cosmic": "Boîte d'aventure cosmique",
"box.adventure.cosmic.desc": "L'univers est une boite. Cette boite est un univers.", "box.adventure.cosmic.desc": "L'univers est une boîte. Cette boîte est un univers.",
"box.adventure.microscopic": "Boite d'aventure microscopique", "box.adventure.microscopic": "Boîte d'aventure microscopique",
"box.adventure.microscopic.desc": "La taille ne compte pas. Sauf quand si. Zoom !", "box.adventure.microscopic.desc": "La taille ne compte pas. Sauf quand si. Zoom !",
"box.adventure.darkfantasy": "Boite d'aventure dark fantasy", "box.adventure.darkfantasy": "Boîte d'aventure dark fantasy",
"box.adventure.darkfantasy.desc": "Les tenebres t'attendent. Et aussi une boite. Une boite sombre.", "box.adventure.darkfantasy.desc": "Les ténèbres t'attendent. Et aussi une boîte. Une boîte sombre.",
"meta.unlocked": "NOUVELLE FONCTIONNALITE : {0} !", "meta.unlocked": "NOUVELLE FONCTIONNALITÉ : {0} !",
"meta.colors": "Couleurs de texte", "meta.colors": "Couleurs de texte",
"meta.extended_colors": "Palette de couleurs etendue", "meta.extended_colors": "Palette de couleurs étendue",
"meta.arrows": "Navigation avec les fleches", "meta.arrows": "Navigation avec les flèches",
"meta.inventory": "Panneau d'inventaire", "meta.inventory": "Panneau d'inventaire",
"meta.resources": "Panneau de ressources", "meta.resources": "Panneau de ressources",
"meta.stats": "Panneau de statistiques", "meta.stats": "Panneau de statistiques",
"meta.portrait": "Panneau portrait", "meta.portrait": "Panneau portrait",
"meta.chat": "Panneau de discussion", "meta.chat": "Panneau de discussion",
"meta.layout": "Mise en page complete", "meta.layout": "Mise en page complète",
"meta.shortcuts": "Raccourcis clavier", "meta.shortcuts": "Raccourcis clavier",
"meta.animation": "Animation d'ouverture de boite", "meta.animation": "Animation d'ouverture de boîte",
"meta.crafting": "Panneau de fabrication", "meta.crafting": "Panneau de fabrication",
"meta.completion": "Suivi de completion", "meta.completion": "Suivi de complétion",
"item.rarity.common": "Commun", "item.rarity.common": "Commun",
"item.rarity.uncommon": "Peu commun", "item.rarity.uncommon": "Peu commun",
"item.rarity.rare": "Rare", "item.rarity.rare": "Rare",
"item.rarity.epic": "Epique", "item.rarity.epic": "Épique",
"item.rarity.legendary": "Legendaire", "item.rarity.legendary": "Légendaire",
"item.rarity.mythic": "Mythique", "item.rarity.mythic": "Mythique",
"resource.health": "Sante", "resource.health": "Santé",
"resource.mana": "Mana", "resource.mana": "Mana",
"resource.food": "Nourriture", "resource.food": "Nourriture",
"resource.stamina": "Endurance", "resource.stamina": "Endurance",
"resource.blood": "Sang", "resource.blood": "Sang",
"resource.gold": "Or", "resource.gold": "Or",
"resource.oxygen": "Oxygene", "resource.oxygen": "Oxygène",
"resource.energy": "Energie", "resource.energy": "Énergie",
"stat.strength": "Force", "stat.strength": "Force",
"stat.intelligence": "Intelligence", "stat.intelligence": "Intelligence",
"stat.luck": "Chance", "stat.luck": "Chance",
"stat.charisma": "Charisme", "stat.charisma": "Charisme",
"stat.dexterity": "Dexterite", "stat.dexterity": "Dextérité",
"stat.wisdom": "Sagesse", "stat.wisdom": "Sagesse",
"item.meta_font_consolas": "Police : Consolas", "item.meta_font_consolas": "Police : Consolas",
@ -154,10 +154,10 @@
"cosmetic.hair.long": "Cheveux longs", "cosmetic.hair.long": "Cheveux longs",
"cosmetic.hair.ponytail": "Queue de cheval", "cosmetic.hair.ponytail": "Queue de cheval",
"cosmetic.hair.braided": "Tresses", "cosmetic.hair.braided": "Tresses",
"cosmetic.hair.cyberpunk": "Cheveux neon cyberpunk", "cosmetic.hair.cyberpunk": "Cheveux néon cyberpunk",
"cosmetic.hair.fire": "Cheveux en feu", "cosmetic.hair.fire": "Cheveux en feu",
"cosmetic.hair.stardust": "Coiffure Poussiere d'Etoile legendaire", "cosmetic.hair.stardust": "Coiffure Poussière d'Étoile légendaire",
"cosmetic.eyes.none": "Pas d'yeux (mysterieux !)", "cosmetic.eyes.none": "Pas d'yeux (mystérieux !)",
"cosmetic.eyes.blue": "Yeux bleus", "cosmetic.eyes.blue": "Yeux bleus",
"cosmetic.eyes.green": "Yeux verts", "cosmetic.eyes.green": "Yeux verts",
"cosmetic.eyes.redorange": "Yeux rouge-orange", "cosmetic.eyes.redorange": "Yeux rouge-orange",
@ -166,31 +166,31 @@
"cosmetic.eyes.sunglasses": "Lunettes de soleil", "cosmetic.eyes.sunglasses": "Lunettes de soleil",
"cosmetic.eyes.pilotglasses": "Lunettes d'aviateur", "cosmetic.eyes.pilotglasses": "Lunettes d'aviateur",
"cosmetic.eyes.aircraftglasses": "Lunettes de pilote de chasse", "cosmetic.eyes.aircraftglasses": "Lunettes de pilote de chasse",
"cosmetic.eyes.cybernetic": "Yeux cybernetiques", "cosmetic.eyes.cybernetic": "Yeux cybernétiques",
"cosmetic.eyes.magician": "Lunettes de magicien", "cosmetic.eyes.magician": "Lunettes de magicien",
"cosmetic.body.naked": "Torse nu", "cosmetic.body.naked": "Torse nu",
"cosmetic.body.regulartshirt": "T-shirt basique", "cosmetic.body.regulartshirt": "T-shirt basique",
"cosmetic.body.sexytshirt": "T-shirt sexy", "cosmetic.body.sexytshirt": "T-shirt sexy",
"cosmetic.body.suit": "Costume", "cosmetic.body.suit": "Costume",
"cosmetic.body.armored": "Armure", "cosmetic.body.armored": "Armure",
"cosmetic.body.robotic": "Chassis robotique", "cosmetic.body.robotic": "Châssis robotique",
"cosmetic.legs.none": "Flottant (pas de jambes !)", "cosmetic.legs.none": "Flottant (pas de jambes !)",
"cosmetic.legs.naked": "Jambes nues", "cosmetic.legs.naked": "Jambes nues",
"cosmetic.legs.slip": "Slip", "cosmetic.legs.slip": "Slip",
"cosmetic.legs.short": "Short", "cosmetic.legs.short": "Short",
"cosmetic.legs.panty": "Culotte", "cosmetic.legs.panty": "Culotte",
"cosmetic.legs.rocketboots": "Bottes a reaction", "cosmetic.legs.rocketboots": "Bottes à réaction",
"cosmetic.legs.pegleg": "Jambe de bois", "cosmetic.legs.pegleg": "Jambe de bois",
"cosmetic.legs.tentacles": "Tentacules", "cosmetic.legs.tentacles": "Tentacules",
"cosmetic.arms.none": "Pas de bras (mode T-Rex)", "cosmetic.arms.none": "Pas de bras (mode T-Rex)",
"cosmetic.arms.short": "Bras courts", "cosmetic.arms.short": "Bras courts",
"cosmetic.arms.regular": "Bras normaux", "cosmetic.arms.regular": "Bras normaux",
"cosmetic.arms.long": "Bras longs extensibles", "cosmetic.arms.long": "Bras longs extensibles",
"cosmetic.arms.mechanical": "Bras mecaniques", "cosmetic.arms.mechanical": "Bras mécaniques",
"cosmetic.arms.wings": "Ailes", "cosmetic.arms.wings": "Ailes",
"cosmetic.arms.extrapair": "Quatre bras", "cosmetic.arms.extrapair": "Quatre bras",
"cosmetic.gender_error": "Nouveau genre (ERREUR : les boites n'ont pas de genre. La boite s'excuse pour la confusion.)", "cosmetic.gender_error": "Nouveau genre (ERREUR : les boîtes n'ont pas de genre. La boîte s'excuse pour la confusion.)",
"tint.none": "Naturel", "tint.none": "Naturel",
"tint.cyan": "Cyan", "tint.cyan": "Cyan",
@ -200,10 +200,10 @@
"tint.light": "Clair", "tint.light": "Clair",
"tint.dark": "Sombre", "tint.dark": "Sombre",
"tint.rainbow": "Arc-en-ciel", "tint.rainbow": "Arc-en-ciel",
"tint.neon": "Neon", "tint.neon": "Néon",
"tint.silver": "Argent", "tint.silver": "Argent",
"tint.gold": "Or", "tint.gold": "Or",
"tint.void": "Neant", "tint.void": "Néant",
"material.wood": "Bois", "material.wood": "Bois",
"material.bronze": "Bronze", "material.bronze": "Bronze",
@ -213,7 +213,7 @@
"material.diamond": "Diamant", "material.diamond": "Diamant",
"material.carbonfiber": "Fibre de carbone", "material.carbonfiber": "Fibre de carbone",
"material.form.raw": "Brut", "material.form.raw": "Brut",
"material.form.refined": "Raffine", "material.form.refined": "Raffiné",
"material.form.nail": "Clou", "material.form.nail": "Clou",
"material.form.plank": "Planche", "material.form.plank": "Planche",
"material.form.ingot": "Lingot", "material.form.ingot": "Lingot",
@ -222,118 +222,118 @@
"material.form.dust": "Poudre", "material.form.dust": "Poudre",
"material.form.gem": "Gemme", "material.form.gem": "Gemme",
"item.health_potion_small": "Petite Potion de Sante", "item.health_potion_small": "Petite Potion de Santé",
"item.health_potion_medium": "Potion de Sante Moyenne", "item.health_potion_medium": "Potion de Santé Moyenne",
"item.health_potion_large": "Grande Potion de Sante", "item.health_potion_large": "Grande Potion de Santé",
"item.mana_crystal_small": "Petit Cristal de Mana", "item.mana_crystal_small": "Petit Cristal de Mana",
"item.mana_crystal_medium": "Cristal de Mana Moyen", "item.mana_crystal_medium": "Cristal de Mana Moyen",
"item.food_ration": "Ration alimentaire", "item.food_ration": "Ration alimentaire",
"item.stamina_drink": "Boisson d'endurance", "item.stamina_drink": "Boisson d'endurance",
"item.blood_vial": "Fiole de sang", "item.blood_vial": "Fiole de sang",
"item.gold_pouch": "Bourse d'or", "item.gold_pouch": "Bourse d'or",
"item.oxygen_tank": "Reservoir d'oxygene", "item.oxygen_tank": "Réservoir d'oxygène",
"item.energy_cell": "Cellule d'energie", "item.energy_cell": "Cellule d'énergie",
"item.space.badge": "Badge d'astronaute", "item.space.badge": "Badge d'astronaute",
"item.space.phone": "Numero de telephone alien", "item.space.phone": "Numéro de téléphone alien",
"item.space.key": "Cle d'acces au sas", "item.space.key": "Clé d'accès au sas",
"item.space.map": "Carte stellaire", "item.space.map": "Carte stellaire",
"item.space.coordinates": "Coordonnees mysterieuses", "item.space.coordinates": "Coordonnées mystérieuses",
"item.space.helmet": "Casque spatial", "item.space.helmet": "Casque spatial",
"item.medieval.crest": "Blason de chevalier", "item.medieval.crest": "Blason de chevalier",
"item.medieval.sword": "Replique d'Excalibur", "item.medieval.sword": "Réplique d'Excalibur",
"item.medieval.scroll": "Parchemin ancien", "item.medieval.scroll": "Parchemin ancien",
"item.medieval.seal": "Sceau royal", "item.medieval.seal": "Sceau royal",
"item.medieval.key": "Cle du donjon", "item.medieval.key": "Clé du donjon",
"item.pirate.map": "Carte au tresor", "item.pirate.map": "Carte au trésor",
"item.pirate.compass": "Boussole enchantee", "item.pirate.compass": "Boussole enchantée",
"item.pirate.feather": "Plume de perroquet", "item.pirate.feather": "Plume de perroquet",
"item.pirate.rum": "Bouteille de rhum", "item.pirate.rum": "Bouteille de rhum",
"item.pirate.flag": "Jolly Roger", "item.pirate.flag": "Jolly Roger",
"item.pirate.key": "Cle du coffre", "item.pirate.key": "Clé du coffre",
"item.contemporary.phone": "Smartphone", "item.contemporary.phone": "Smartphone",
"item.contemporary.card": "Carte de credit", "item.contemporary.card": "Carte de crédit",
"item.contemporary.ticket": "Ticket de metro", "item.contemporary.ticket": "Ticket de métro",
"item.contemporary.usb": "Cle USB suspecte", "item.contemporary.usb": "Clé USB suspecte",
"item.contemporary.key": "Cle d'appartement", "item.contemporary.key": "Clé d'appartement",
"item.contemporary.badge": "Badge d'entreprise", "item.contemporary.badge": "Badge d'entreprise",
"item.sentimental.letter": "Lettre d'amour", "item.sentimental.letter": "Lettre d'amour",
"item.sentimental.flower": "Fleur sechee", "item.sentimental.flower": "Fleur séchée",
"item.sentimental.album": "Album photo", "item.sentimental.album": "Album photo",
"item.sentimental.melody": "Melodie de boite a musique", "item.sentimental.melody": "Mélodie de boîte à musique",
"item.sentimental.teddy": "Vieil ours en peluche", "item.sentimental.teddy": "Vieil ours en peluche",
"item.sentimental.phone": "Numero de l'ex", "item.sentimental.phone": "Numéro de l'ex",
"item.prehistoric.tooth": "Dent de dinosaure", "item.prehistoric.tooth": "Dent de dinosaure",
"item.prehistoric.painting": "Fragment de peinture rupestre", "item.prehistoric.painting": "Fragment de peinture rupestre",
"item.prehistoric.amber": "Pierre d'ambre", "item.prehistoric.amber": "Pierre d'ambre",
"item.prehistoric.club": "Massue en os", "item.prehistoric.club": "Massue en os",
"item.prehistoric.fossil": "Fossile de trilobite", "item.prehistoric.fossil": "Fossile de trilobite",
"item.cosmic.shard": "Eclat de nebuleuse", "item.cosmic.shard": "Éclat de nébuleuse",
"item.cosmic.fragment": "Fragment de trou noir", "item.cosmic.fragment": "Fragment de trou noir",
"item.cosmic.crystal": "Cristal de quasar", "item.cosmic.crystal": "Cristal de quasar",
"item.cosmic.dust": "Poussiere cosmique", "item.cosmic.dust": "Poussière cosmique",
"item.cosmic.core": "Coeur d'etoile", "item.cosmic.core": "Cœur d'étoile",
"item.microscopic.bacteria": "Echantillon de bacterie sentiente", "item.microscopic.bacteria": "Échantillon de bactérie sentiente",
"item.microscopic.dna": "Brin d'ADN luminescent", "item.microscopic.dna": "Brin d'ADN luminescent",
"item.microscopic.membrane": "Membrane cellulaire renforcee", "item.microscopic.membrane": "Membrane cellulaire renforcée",
"item.microscopic.mitochondria": "Mitochondrie hyperactive", "item.microscopic.mitochondria": "Mitochondrie hyperactive",
"item.microscopic.prion": "Prion amical (probablement)", "item.microscopic.prion": "Prion amical (probablement)",
"item.darkfantasy.ring": "Anneau maudit", "item.darkfantasy.ring": "Anneau maudit",
"item.darkfantasy.rune": "Rune de sang", "item.darkfantasy.rune": "Rune de sang",
"item.darkfantasy.cloak": "Cape d'ombre", "item.darkfantasy.cloak": "Cape d'ombre",
"item.darkfantasy.grimoire": "Grimoire du necromancien", "item.darkfantasy.grimoire": "Grimoire du nécromancien",
"item.darkfantasy.gem": "Gemme d'ame", "item.darkfantasy.gem": "Gemme d'âme",
"item.darkfantasy.key": "Cle en os", "item.darkfantasy.key": "Clé en os",
"item.resource_max_up": "{0} Max +1", "item.resource_max_up": "{0} Max +1",
"item.resource_up": "{0} +1", "item.resource_up": "{0} +1",
"item.stat_boost": "{0} +1", "item.stat_boost": "{0} +1",
"item.resource_max_health": "Amelioration Capacite Sante", "item.resource_max_health": "Amélioration Capacité Santé",
"item.resource_max_mana": "Amelioration Capacite Mana", "item.resource_max_mana": "Amélioration Capacité Mana",
"item.resource_max_food": "Amelioration Capacite Nourriture", "item.resource_max_food": "Amélioration Capacité Nourriture",
"item.resource_max_stamina": "Amelioration Capacite Endurance", "item.resource_max_stamina": "Amélioration Capacité Endurance",
"item.resource_max_gold": "Amelioration Capacite Or", "item.resource_max_gold": "Amélioration Capacité Or",
"item.resource_max_blood": "Amelioration Capacite Sang", "item.resource_max_blood": "Amélioration Capacité Sang",
"item.resource_max_oxygen": "Amelioration Capacite Oxygene", "item.resource_max_oxygen": "Amélioration Capacité Oxygène",
"item.resource_max_energy": "Amelioration Capacite Energie", "item.resource_max_energy": "Amélioration Capacité Énergie",
"item.music_melody": "Melodie de boite", "item.music_melody": "Mélodie de boîte",
"item.cookie_fortune": "Fortune Cookie", "item.cookie_fortune": "Fortune Cookie",
"item.mysterious_key": "Cle mysterieuse", "item.mysterious_key": "Clé mystérieuse",
"item.mysterious_key.desc": "Une cle pour... quelque chose. La boite sait, mais la boite ne parle pas.", "item.mysterious_key.desc": "Une clé pour... quelque chose. La boîte sait, mais la boîte ne parle pas.",
"lore.fragment_1": "Au commencement, il y avait une boite. La boite contenait une autre boite. Et c'est ainsi que ca a ete, et que ca sera.", "lore.fragment_1": "Au commencement, il y avait une boîte. La boîte contenait une autre boîte. Et c'est ainsi que ça a été, et que ça sera.",
"lore.fragment_2": "L'Ancien Ordre des Ouvreurs de Boites n'a qu'un seul commandement : Tu ouvriras tes boites.", "lore.fragment_2": "L'Ancien Ordre des Ouvreurs de Boîtes n'a qu'un seul commandement : Tu ouvriras tes boîtes.",
"lore.fragment_3": "Certains disent que l'univers lui-meme est une boite, attendant d'etre ouverte par quelqu'un d'assez curieux.", "lore.fragment_3": "Certains disent que l'univers lui-même est une boîte, attendant d'être ouverte par quelqu'un d'assez curieux.",
"lore.fragment_4": "La premiere boite a ete ouverte par Farah, qui a trouve a l'interieur le concept d''interieur'.", "lore.fragment_4": "La première boîte a été ouverte par Farah, qui a trouvé à l'intérieur le concept d'« intérieur ».",
"lore.fragment_5": "Malkith a un jour ouvert une boite contenant le son d'une seule main qui applaudit. Personne ne sait ce que ca veut dire.", "lore.fragment_5": "Malkith a un jour ouvert une boîte contenant le son d'une seule main qui applaudit. Personne ne sait ce que ça veut dire.",
"lore.fragment_6": "La legende dit qu'il existe une boite qui contient toutes les autres boites. L'ouvrir causerait un paradoxe. Ou un remboursement.", "lore.fragment_6": "La légende dit qu'il existe une boîte qui contient toutes les autres boîtes. L'ouvrir causerait un paradoxe. Ou un remboursement.",
"lore.fragment_7": "Duncan a essaye de fermer une boite un jour. Le syndicat des boites s'est mis en greve pendant trois semaines.", "lore.fragment_7": "Duncan a essayé de fermer une boîte un jour. Le syndicat des boîtes s'est mis en grève pendant trois semaines.",
"lore.fragment_8": "Pierrick a construit une machine a ouvrir des boites. Elle s'est ouverte elle-meme. Puis elle a ouvert la machine. Puis elle a ouvert le concept d'ouverture.", "lore.fragment_8": "Pierrick a construit une machine à ouvrir des boîtes. Elle s'est ouverte elle-même. Puis elle a ouvert la machine. Puis elle a ouvert le concept d'ouverture.",
"lore.fragment_9": "Samuel a ecrit le premier manuel d'ouverture de boites. Chapitre 1 : Ouvre la boite. Chapitre 2 : Voir Chapitre 1.", "lore.fragment_9": "Samuel a écrit le premier manuel d'ouverture de boîtes. Chapitre 1 : Ouvre la boîte. Chapitre 2 : Voir Chapitre 1.",
"lore.fragment_10": "La Boite Noire contient un chat. Ou pas. Jusqu'a ce que tu l'ouvres, elle en contient et n'en contient pas. Le chat est aussi une boite.", "lore.fragment_10": "La Boîte Noire contient un chat. Ou pas. Jusqu'à ce que tu l'ouvres, elle en contient et n'en contient pas. Le chat est aussi une boîte.",
"cookie.1": "Une boite dans une boite reste une boite.", "cookie.1": "Une boîte dans une boîte reste une boîte.",
"cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Reessayez.", "cookie.2": "ERREUR : Ce cookie ne contient aucune fortune. Réessayez.",
"cookie.3": "Vous ouvrirez beaucoup de boites. Cette prediction a un taux de precision de 100%.", "cookie.3": "Vous ouvrirez beaucoup de boîtes. Cette prédiction a un taux de précision de 100%.",
"cookie.4": "Le vrai tresor, c'etait les boites qu'on a ouvertes en chemin.", "cookie.4": "Le vrai trésor, c'était les boîtes qu'on a ouvertes en chemin.",
"cookie.5": "ATTENTION : Les effets secondaires de l'ouverture de boites incluent la joie, la confusion et des bras-tentacules.", "cookie.5": "ATTENTION : Les effets secondaires de l'ouverture de boîtes incluent la joie, la confusion et des bras-tentacules.",
"cookie.6": "Demain tu trouveras une boite. Puis une autre. Puis une autre. Envoyez de l'aide.", "cookie.6": "Demain tu trouveras une boîte. Puis une autre. Puis une autre. Envoyez de l'aide.",
"cookie.7": "Ton nombre porte-bonheur est le nombre de boites que tu as ouvertes. Donc... beaucoup.", "cookie.7": "Ton nombre porte-bonheur est le nombre de boîtes que tu as ouvertes. Donc... beaucoup.",
"cookie.8": "Confucius dit : celui qui ouvre boite trouve boite. Celui qui n'ouvre pas boite trouve aussi boite. Boite est inevitable.", "cookie.8": "Confucius dit : celui qui ouvre boîte trouve boîte. Celui qui n'ouvre pas boîte trouve aussi boîte. Boîte est inévitable.",
"cookie.9": "Un voyage de mille boites commence par une seule ouverture.", "cookie.9": "Un voyage de mille boîtes commence par une seule ouverture.",
"cookie.10": "Si tu lis ceci, tu as passe trop de temps a ouvrir des boites. Je rigole, ca n'existe pas.", "cookie.10": "Si tu lis ceci, tu as passé trop de temps à ouvrir des boîtes. Je rigole, ça n'existe pas.",
"cookie.11": "La boite donne, et la boite redonne des boites.", "cookie.11": "La boîte donne, et la boîte redonne des boîtes.",
"cookie.12": "En Russie sovietique, c'est la boite qui t'ouvre.", "cookie.12": "En Russie soviétique, c'est la boîte qui t'ouvre.",
"cookie.13": "Au secours je suis piege dans une usine a fortune cookies a l'interieur d'une boite.", "cookie.13": "Au secours je suis piégé dans une usine à fortune cookies à l'intérieur d'une boîte.",
"cookie.14": "Cette fortune a ete intentionnellement laissee vide. Je rigole. Ou pas ?", "cookie.14": "Cette fortune a été intentionnellement laissée vide. Je rigole. Ou pas ?",
"cookie.15": "Tu es l'elu. Celui qui ouvre les boites. Vraiment une noble vocation.", "cookie.15": "Tu es l'élu. Celui qui ouvre les boîtes. Vraiment une noble vocation.",
"cookie.16": "Plot twist : la boite c'etait les amis qu'on s'est faits en chemin.", "cookie.16": "Plot twist : la boîte c'était les amis qu'on s'est faits en chemin.",
"cookie.17": "Schrodinger a appele. Il veut recuperer son concept de boite.", "cookie.17": "Schrödinger a appelé. Il veut récupérer son concept de boîte.",
"cookie.18": "Si tu ouvres une boite et que personne n'est la pour l'entendre, est-ce que ca fait un loot ?", "cookie.18": "Si tu ouvres une boîte et que personne n'est là pour l'entendre, est-ce que ça fait un loot ?",
"cookie.19": "Aujourd'hui est un bon jour pour ouvrir des boites. Demain aussi. Tous les jours, en fait.", "cookie.19": "Aujourd'hui est un bon jour pour ouvrir des boîtes. Demain aussi. Tous les jours, en fait.",
"cookie.20": "Ton animal totem est une boite. Ton pouvoir special c'est l'ouverture.", "cookie.20": "Ton animal totem est une boîte. Ton pouvoir spécial c'est l'ouverture.",
"character.farah": "Farah", "character.farah": "Farah",
"character.malkith": "Malkith", "character.malkith": "Malkith",
@ -345,36 +345,36 @@
"character.pierrick": "Pierrick", "character.pierrick": "Pierrick",
"character.nova": "Capitaine Nova", "character.nova": "Capitaine Nova",
"character.aria": "ARIA", "character.aria": "ARIA",
"character.blackbeard": "Barbe-Noire l'Indeboitable", "character.blackbeard": "Barbe-Noire l'Indéboîtable",
"character.mordecai": "Mordecai le Sinistre", "character.mordecai": "Mordecaï le Sinistre",
"character.zephyr": "Zephyr", "character.zephyr": "Zéphyr",
"character.quantum": "Dr. Quantum", "character.quantum": "Dr. Quantum",
"adventure.start": "Commencer l'aventure {0}", "adventure.start": "Commencer l'aventure {0}",
"adventure.resume": "Reprendre l'aventure {0}", "adventure.resume": "Reprendre l'aventure {0}",
"adventure.completed": "Aventure terminee ! Tu es maintenant un aventurier de boites certifie.", "adventure.completed": "Aventure terminée ! Tu es maintenant un aventurier de boîtes certifié.",
"adventure.item_granted": "Recu : {0} x{1}", "adventure.item_granted": "Reçu : {0} x{1}",
"adventure.item_removed": "Perdu : {0}", "adventure.item_removed": "Perdu : {0}",
"adventure.resource_added": "{0} +{1}", "adventure.resource_added": "{0} +{1}",
"interaction.key_chest": "La cle rentre ! Le coffre s'ouvre automatiquement !", "interaction.key_chest": "La clé rentre ! Le coffre s'ouvre automatiquement !",
"interaction.key_no_match": "Cette cle semble ouvrir quelque chose... mais tu ne l'as pas encore. Peut-etre qu'une future boite le fournira.", "interaction.key_no_match": "Cette clé semble ouvrir quelque chose... mais tu ne l'as pas encore. Peut-être qu'une future boîte le fournira.",
"interaction.craft_available": "Nouvelle recette disponible a {0} !", "interaction.craft_available": "Nouvelle recette disponible à {0} !",
"save.saving": "Sauvegarde en cours...", "save.saving": "Sauvegarde en cours...",
"save.saved": "Partie sauvegardee dans l'emplacement '{0}'.", "save.saved": "Partie sauvegardée dans l'emplacement '{0}'.",
"save.loading": "Chargement...", "save.loading": "Chargement...",
"save.loaded": "Partie chargee depuis l'emplacement '{0}'.", "save.loaded": "Partie chargée depuis l'emplacement '{0}'.",
"save.no_saves": "Aucune sauvegarde trouvee.", "save.no_saves": "Aucune sauvegarde trouvée.",
"save.choose_slot": "Choisis un emplacement de sauvegarde :", "save.choose_slot": "Choisis un emplacement de sauvegarde :",
"error.invalid_input": "Entree invalide. Reessaie, brave ouvreur de boites.", "error.invalid_input": "Entrée invalide. Réessaie, brave ouvreur de boîtes.",
"error.no_boxes": "Tu n'as aucune boite a ouvrir. Comment t'as fait ? Ouvre plus de boites pour avoir des boites.", "error.no_boxes": "Tu n'as aucune boîte à ouvrir. Comment t'as fait ? Ouvre plus de boîtes pour avoir des boîtes.",
"error.not_enough_resources": "Pas assez de {0}. Il t'en manque {1}.", "error.not_enough_resources": "Pas assez de {0}. Il t'en manque {1}.",
"misc.boxes_opened": "Total de boites ouvertes : {0}", "misc.boxes_opened": "Total de boîtes ouvertes : {0}",
"misc.play_time": "Temps de jeu : {0}", "misc.play_time": "Temps de jeu : {0}",
"misc.welcome_back": "Bon retour, {0} ! Tes boites se sont ennuyees.", "misc.welcome_back": "Bon retour, {0} ! Tes boîtes se sont ennuyées.",
"recipe.refine_wood": "Raffiner le bois", "recipe.refine_wood": "Raffiner le bois",
"recipe.smelt_bronze_ingot": "Fondre un lingot de bronze", "recipe.smelt_bronze_ingot": "Fondre un lingot de bronze",
@ -382,48 +382,50 @@
"recipe.smelt_steel_ingot": "Fondre un lingot d'acier", "recipe.smelt_steel_ingot": "Fondre un lingot d'acier",
"recipe.smelt_titanium_ingot": "Fondre un lingot de titane", "recipe.smelt_titanium_ingot": "Fondre un lingot de titane",
"recipe.forge_carbonfiber_sheet": "Presser une feuille de fibre de carbone", "recipe.forge_carbonfiber_sheet": "Presser une feuille de fibre de carbone",
"recipe.brew_health_potion_medium": "Brasser une potion de sante moyenne", "recipe.brew_health_potion_medium": "Brasser une potion de santé moyenne",
"recipe.brew_mana_crystal_medium": "Raffiner un cristal de mana moyen", "recipe.brew_mana_crystal_medium": "Raffiner un cristal de mana moyen",
"recipe.synthesize_energy_cell": "Synthetiser une cellule d'energie", "recipe.synthesize_energy_cell": "Synthétiser une cellule d'énergie",
"recipe.pressurize_oxygen_tank": "Pressuriser un reservoir d'oxygene", "recipe.pressurize_oxygen_tank": "Pressuriser un réservoir d'oxygène",
"recipe.craft_pilot_glasses": "Fabriquer des lunettes d'aviateur", "recipe.craft_pilot_glasses": "Fabriquer des lunettes d'aviateur",
"recipe.forge_armored_plate": "Forger une armure", "recipe.forge_armored_plate": "Forger une armure",
"recipe.engineer_rocket_boots": "Concevoir des bottes a reaction", "recipe.engineer_rocket_boots": "Concevoir des bottes à réaction",
"recipe.chart_star_navigation": "Cartographier la navigation stellaire", "recipe.chart_star_navigation": "Cartographier la navigation stellaire",
"recipe.engrave_royal_seal": "Graver un sceau royal", "recipe.engrave_royal_seal": "Graver un sceau royal",
"recipe.enchant_dark_grimoire": "Enchanter le grimoire sombre", "recipe.enchant_dark_grimoire": "Enchanter le grimoire sombre",
"recipe.fuse_cosmic_crystal": "Fusionner un cristal cosmique", "recipe.fuse_cosmic_crystal": "Fusionner un cristal cosmique",
"recipe.splice_glowing_dna": "Episser de l'ADN luminescent", "recipe.splice_glowing_dna": "Épisser de l'ADN luminescent",
"recipe.preserve_amber": "Conserver une pierre d'ambre", "recipe.preserve_amber": "Conserver une pierre d'ambre",
"recipe.craft_box_ok_tier": "Fabriquer une boite ok tiers", "recipe.craft_box_ok_tier": "Fabriquer une boîte ok tiers",
"recipe.craft_box_cool": "Fabriquer une boite coolos", "recipe.craft_box_cool": "Fabriquer une boîte coolos",
"recipe.craft_box_supply": "Fabriquer une boite de fourniture", "recipe.craft_box_supply": "Fabriquer une boîte de fourniture",
"recipe.craft_box_epic": "Fabriquer une boite epique", "recipe.craft_box_epic": "Fabriquer une boîte épique",
"action.collect_crafting": "Recuperer les fabrications", "action.collect_crafting": "Récupérer les fabrications",
"craft.started": "Fabrication auto : {0} a l'atelier {1}", "craft.started": "Fabrication auto : {0} à l'atelier {1}",
"craft.completed": "{0} a termine la fabrication !", "craft.completed": "{0} a terminé la fabrication !",
"craft.done": "Termine", "craft.done": "Terminé",
"craft.panel.title": "Ateliers", "craft.panel.title": "Ateliers",
"craft.panel.empty": "Aucun atelier en activite.", "craft.panel.empty": "Aucun atelier en activité.",
"item.blueprint.foundry": "Plan de Fonderie", "item.blueprint.foundry": "Plan de Fonderie",
"item.blueprint.workbench": "Plan d'Etabli", "item.blueprint.workbench": "Plan d'Établi",
"item.blueprint.furnace": "Plan de Fourneau", "item.blueprint.furnace": "Plan de Fourneau",
"item.blueprint.forge": "Plan de Forge", "item.blueprint.forge": "Plan de Forge",
"item.blueprint.alchemy": "Plan de Table d'Alchimie", "item.blueprint.alchemy": "Plan de Table d'Alchimie",
"item.blueprint.engineer": "Plan de Bureau d'Ingenieur", "item.blueprint.engineer": "Plan de Bureau d'Ingénieur",
"item.blueprint.drawing": "Plan de Table a Dessin", "item.blueprint.drawing": "Plan de Table à Dessin",
"item.blueprint.engraving": "Plan de Banc de Gravure", "item.blueprint.engraving": "Plan de Banc de Gravure",
"item.blueprint.pentacle": "Plan de Pentacle de Transformation", "item.blueprint.pentacle": "Plan de Pentacle de Transformation",
"item.blueprint.printer": "Plan d'Imprimante 3D", "item.blueprint.printer": "Plan d'Imprimante 3D",
"item.blueprint.synthesizer": "Plan de Synthetiseur de Matiere", "item.blueprint.synthesizer": "Plan de Synthétiseur de Matière",
"item.blueprint.genetic": "Plan de Station de Modification Genetique", "item.blueprint.genetic": "Plan de Station de Modification Génétique",
"item.blueprint.stasis": "Plan de Chambre de Stase", "item.blueprint.stasis": "Plan de Chambre de Stase",
"box.endgame": "La Boite Finale", "box.endgame": "La Boîte Finale",
"box.endgame.desc": "Tu as trouve toutes les ressources. C'est la derniere boite. Es-tu pret ?", "box.endgame.desc": "Tu as trouvé toutes les ressources. C'est la dernière boîte. Es-tu prêt ?",
"item.endgame_crown": "Couronne d'Accomplissement", "item.endgame_crown": "Couronne d'Accomplissement",
"item.destiny_token": "Jeton du Destin", "item.destiny_token": "Jeton du Destin",
"adventure.secret_branch_found": "Tu sens un chemin secret se reveler..." "adventure.secret_branch_found": "Tu sens un chemin secret se révéler...",
"meta.autosave": "Sauvegarde automatique",
"save.autosaved": "Partie sauvegardée automatiquement."
} }

View file

@ -44,5 +44,8 @@ public enum UIFeature
CraftingPanel, CraftingPanel,
/// <summary>Phase 5: Shows completion percentage of all unique content.</summary> /// <summary>Phase 5: Shows completion percentage of all unique content.</summary>
CompletionTracker CompletionTracker,
/// <summary>Phase 2: Automatic save when returning to the hub. Removes the manual save action.</summary>
AutoSave
} }

View file

@ -66,6 +66,31 @@ public static class Program
private static async Task MainMenuLoop() private static async Task MainMenuLoop()
{ {
// Check for existing saves to determine startup flow
var existingSaves = _saveManager.ListSlots();
if (existingSaves.Count > 0)
{
// Saves exist: load locale from the most recent save, skip language prompt
var mostRecent = existingSaves[0]; // Already sorted by SavedAt descending
var recentState = _saveManager.Load(mostRecent.Name);
if (recentState != null)
{
_loc.Change(recentState.CurrentLocale);
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
}
}
else
{
// No saves: prompt language first
_renderer.Clear();
var langOptions = new List<string> { "English", "Français" };
int langChoice = _renderer.ShowSelection("Language / Langue", langOptions);
var selectedLocale = langChoice == 0 ? Locale.EN : Locale.FR;
_loc.Change(selectedLocale);
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
}
while (_running) while (_running)
{ {
_renderer.Clear(); _renderer.Clear();
@ -76,26 +101,79 @@ public static class Program
_renderer.ShowMessage(_loc.Get("game.subtitle")); _renderer.ShowMessage(_loc.Get("game.subtitle"));
_renderer.ShowMessage(""); _renderer.ShowMessage("");
var options = new List<string> // Rebuild saves list (may have changed after new game / save)
existingSaves = _saveManager.ListSlots();
var options = new List<string>();
var actions = new List<string>();
// If saves exist, show "Continuer" as first option with most recent save info
if (existingSaves.Count > 0)
{ {
_loc.Get("menu.new_game"), var recent = existingSaves[0];
_loc.Get("menu.load_game"), var savedAt = recent.SavedAt.ToLocalTime();
_loc.Get("menu.language"), options.Add($"{_loc.Get("menu.continue")} ({recent.Name} {savedAt:dd/MM/yyyy HH:mm})");
_loc.Get("menu.quit") actions.Add("continue");
}; }
options.Add(_loc.Get("menu.new_game"));
actions.Add("new_game");
if (existingSaves.Count > 1)
{
options.Add(_loc.Get("menu.load_game"));
actions.Add("load_game");
}
options.Add(_loc.Get("menu.language"));
actions.Add("language");
options.Add(_loc.Get("menu.quit"));
actions.Add("quit");
int choice = _renderer.ShowSelection("", options); int choice = _renderer.ShowSelection("", options);
switch (choice) switch (actions[choice])
{ {
case 0: await NewGame(); break; case "continue":
case 1: await LoadGame(); break; await ContinueGame(existingSaves[0].Name);
case 2: ChangeLanguage(); break; break;
case 3: _running = false; break; case "new_game":
await NewGame();
break;
case "load_game":
await LoadGame();
break;
case "language":
ChangeLanguage();
break;
case "quit":
_running = false;
break;
} }
} }
} }
private static async Task ContinueGame(string slotName)
{
var loaded = _saveManager.Load(slotName);
if (loaded == null)
{
_renderer.ShowError("Failed to load save.");
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
return;
}
_state = loaded;
_loc.Change(_state.CurrentLocale);
InitializeGame();
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
await GameLoop();
}
private static async Task NewGame() private static async Task NewGame()
{ {
string name = _renderer.ShowTextInput(_loc.Get("prompt.name")); string name = _renderer.ShowTextInput(_loc.Get("prompt.name"));
@ -181,6 +259,12 @@ public static class Program
{ {
while (_running) while (_running)
{ {
// Auto-save when returning to the hub (if the feature is unlocked)
if (_state.HasUIFeature(UIFeature.AutoSave))
{
_saveManager.Save(_state, _state.PlayerName);
}
// Tick crafting jobs (InProgress → Completed) // Tick crafting jobs (InProgress → Completed)
TickCraftingJobs(); TickCraftingJobs();
@ -225,7 +309,9 @@ public static class Program
if (completedJobs.Count > 0) if (completedJobs.Count > 0)
actions.Add((_loc.Get("action.collect_crafting") + $" ({completedJobs.Count})", "collect_crafting")); actions.Add((_loc.Get("action.collect_crafting") + $" ({completedJobs.Count})", "collect_crafting"));
actions.Add((_loc.Get("action.save"), "save")); if (!_state.HasUIFeature(UIFeature.AutoSave))
actions.Add((_loc.Get("action.save"), "save"));
actions.Add((_loc.Get("action.quit"), "quit")); actions.Add((_loc.Get("action.quit"), "quit"));
return actions; return actions;
@ -283,6 +369,12 @@ public static class Program
// so we don't show "You received" for items the player never actually gets // so we don't show "You received" for items the player never actually gets
var autoConsumedIds = events.OfType<ItemConsumedEvent>().Select(e => e.InstanceId).ToHashSet(); var autoConsumedIds = events.OfType<ItemConsumedEvent>().Select(e => e.InstanceId).ToHashSet();
// Collect all received items to show as a single grouped loot reveal
var allLoot = new List<(string name, string rarity, string category)>();
// Show only the primary box opening (not auto-opened intermediaries)
bool primaryBoxShown = false;
foreach (var evt in events) foreach (var evt in events)
{ {
switch (evt) switch (evt)
@ -290,14 +382,12 @@ public static class Program
case BoxOpenedEvent boxEvt: case BoxOpenedEvent boxEvt:
var boxDef = _registry.GetBox(boxEvt.BoxId); var boxDef = _registry.GetBox(boxEvt.BoxId);
var boxName = _loc.Get(boxDef?.NameKey ?? boxEvt.BoxId); var boxName = _loc.Get(boxDef?.NameKey ?? boxEvt.BoxId);
if (boxEvt.IsAutoOpen) if (!boxEvt.IsAutoOpen && !primaryBoxShown)
{
_renderer.ShowMessage(_loc.Get("box.auto_open", boxName));
}
else
{ {
_renderer.ShowBoxOpening(boxName, boxDef?.Rarity.ToString() ?? "Common"); _renderer.ShowBoxOpening(boxName, boxDef?.Rarity.ToString() ?? "Common");
primaryBoxShown = true;
} }
// Auto-opened boxes are silent — their loot appears in the grouped reveal
break; break;
case ItemReceivedEvent itemEvt: case ItemReceivedEvent itemEvt:
@ -306,14 +396,11 @@ public static class Program
break; break;
var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId); var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId);
var itemBoxDef = itemDef is null ? _registry.GetBox(itemEvt.Item.DefinitionId) : null; var itemBoxDef = itemDef is null ? _registry.GetBox(itemEvt.Item.DefinitionId) : null;
_renderer.ShowLootReveal( allLoot.Add((
[ GetLocalizedName(itemEvt.Item.DefinitionId),
( (itemDef?.Rarity ?? itemBoxDef?.Rarity ?? ItemRarity.Common).ToString(),
GetLocalizedName(itemEvt.Item.DefinitionId), (itemDef?.Category ?? ItemCategory.Box).ToString()
(itemDef?.Rarity ?? itemBoxDef?.Rarity ?? ItemRarity.Common).ToString(), ));
(itemDef?.Category ?? ItemCategory.Box).ToString()
)
]);
break; break;
case UIFeatureUnlockedEvent uiEvt: case UIFeatureUnlockedEvent uiEvt:
@ -377,6 +464,12 @@ public static class Program
} }
} }
// Show all received loot as a single grouped reveal
if (allLoot.Count > 0)
{
_renderer.ShowLootReveal(allLoot);
}
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); _renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
} }
@ -571,6 +664,7 @@ public static class Program
UIFeature.BoxAnimation => "meta.animation", UIFeature.BoxAnimation => "meta.animation",
UIFeature.CraftingPanel => "meta.crafting", UIFeature.CraftingPanel => "meta.crafting",
UIFeature.CompletionTracker => "meta.completion", UIFeature.CompletionTracker => "meta.completion",
UIFeature.AutoSave => "meta.autosave",
_ => $"meta.{feature.ToString().ToLower()}" _ => $"meta.{feature.ToString().ToLower()}"
}; };

View file

@ -25,7 +25,7 @@ public sealed class BasicRenderer(LocalizationManager loc) : IRenderer
Console.WriteLine(loc.Get("box.opening", boxName)); Console.WriteLine(loc.Get("box.opening", boxName));
Console.WriteLine("..."); Console.WriteLine("...");
Console.WriteLine("......"); Console.WriteLine("......");
Console.WriteLine(loc.Get("box.opened", boxName, rarity)); Console.WriteLine(loc.Get("box.opened_short", boxName));
} }
public void ShowLootReveal(List<(string name, string rarity, string category)> items) public void ShowLootReveal(List<(string name, string rarity, string category)> items)
@ -34,7 +34,7 @@ public sealed class BasicRenderer(LocalizationManager loc) : IRenderer
for (int i = 0; i < items.Count; i++) for (int i = 0; i < items.Count; i++)
{ {
var (name, rarity, category) = items[i]; var (name, rarity, category) = items[i];
Console.WriteLine($" - {name} [{rarity}] ({category})"); Console.WriteLine($" - {name} [{rarity}]");
} }
} }

View file

@ -69,7 +69,7 @@ public sealed class SpectreRenderer : IRenderer
{ {
Console.WriteLine(_loc.Get("box.opening", boxName)); Console.WriteLine(_loc.Get("box.opening", boxName));
Thread.Sleep(500); Thread.Sleep(500);
Console.WriteLine(_loc.Get("box.opened", boxName, rarity)); Console.WriteLine(_loc.Get("box.opened_short", boxName));
} }
} }
@ -83,16 +83,14 @@ public sealed class SpectreRenderer : IRenderer
.Border(TableBorder.Rounded) .Border(TableBorder.Rounded)
.Title($"[bold yellow]{Markup.Escape(_loc.Get("loot.title"))}[/]") .Title($"[bold yellow]{Markup.Escape(_loc.Get("loot.title"))}[/]")
.AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.name"))}[/]").Centered()) .AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.name"))}[/]").Centered())
.AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.rarity"))}[/]").Centered()) .AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.rarity"))}[/]").Centered());
.AddColumn(new TableColumn($"[bold]{Markup.Escape(_loc.Get("loot.category"))}[/]").Centered());
foreach (var (name, rarity, category) in items) foreach (var (name, rarity, category) in items)
{ {
string color = RarityColor(rarity); string color = RarityColor(rarity);
table.AddRow( table.AddRow(
$"[{color}]{Markup.Escape(name)}[/]", $"[{color}]{Markup.Escape(name)}[/]",
$"[{color}]{Markup.Escape(rarity)}[/]", $"[{color}]{Markup.Escape(rarity)}[/]");
Markup.Escape(category));
} }
AnsiConsole.Write(table); AnsiConsole.Write(table);
@ -103,7 +101,7 @@ public sealed class SpectreRenderer : IRenderer
foreach (var (name, rarity, category) in items) foreach (var (name, rarity, category) in items)
{ {
string color = RarityColor(rarity); string color = RarityColor(rarity);
AnsiConsole.MarkupLine($" - [{color}]{Markup.Escape(name)}[/] [{color}][[{Markup.Escape(rarity)}]][/] ({Markup.Escape(category)})"); AnsiConsole.MarkupLine($" - [{color}]{Markup.Escape(name)}[/] [{color}][[{Markup.Escape(rarity)}]][/]");
} }
} }
else else
@ -111,7 +109,7 @@ public sealed class SpectreRenderer : IRenderer
Console.WriteLine(_loc.Get("loot.received")); Console.WriteLine(_loc.Get("loot.received"));
foreach (var (name, rarity, category) in items) foreach (var (name, rarity, category) in items)
{ {
Console.WriteLine($" - {name} [{rarity}] ({category})"); Console.WriteLine($" - {name} [{rarity}]");
} }
} }
} }

View file

@ -340,6 +340,129 @@ public class ContentValidationTests
Assert.Empty(missing); Assert.Empty(missing);
} }
[Fact]
public void BoxDescriptionKeys_ExistInLocalization()
{
var boxes = LoadBoxes();
var en = LoadEnStrings();
var missing = boxes
.Where(b => !string.IsNullOrEmpty(b.DescriptionKey) && !en.ContainsKey(b.DescriptionKey))
.Select(b => $"{b.Id} -> descriptionKey '{b.DescriptionKey}'")
.ToList();
Assert.Empty(missing);
}
[Fact]
public void ItemNameKeys_ExistInFrLocalization()
{
var items = LoadItems();
var frJson = File.ReadAllText(FrStringsPath);
var fr = JsonSerializer.Deserialize<Dictionary<string, string>>(frJson)!;
var missing = items
.Where(i => !fr.ContainsKey(i.NameKey))
.Select(i => $"{i.Id} -> nameKey '{i.NameKey}'")
.ToList();
Assert.Empty(missing);
}
[Fact]
public void BoxNameKeys_ExistInFrLocalization()
{
var boxes = LoadBoxes();
var frJson = File.ReadAllText(FrStringsPath);
var fr = JsonSerializer.Deserialize<Dictionary<string, string>>(frJson)!;
var missing = boxes
.Where(b => !fr.ContainsKey(b.NameKey))
.Select(b => $"{b.Id} -> nameKey '{b.NameKey}'")
.ToList();
Assert.Empty(missing);
}
[Fact]
public void BoxDescriptionKeys_ExistInFrLocalization()
{
var boxes = LoadBoxes();
var frJson = File.ReadAllText(FrStringsPath);
var fr = JsonSerializer.Deserialize<Dictionary<string, string>>(frJson)!;
var missing = boxes
.Where(b => !string.IsNullOrEmpty(b.DescriptionKey) && !fr.ContainsKey(b.DescriptionKey))
.Select(b => $"{b.Id} -> descriptionKey '{b.DescriptionKey}'")
.ToList();
Assert.Empty(missing);
}
[Fact]
public void AllUIFeatures_HaveLocalizationKeys()
{
var en = LoadEnStrings();
// Map of UIFeature -> expected localization key
var featureKeys = new Dictionary<UIFeature, string>
{
[UIFeature.TextColors] = "meta.colors",
[UIFeature.ExtendedColors] = "meta.extended_colors",
[UIFeature.ArrowKeySelection] = "meta.arrows",
[UIFeature.InventoryPanel] = "meta.inventory",
[UIFeature.ResourcePanel] = "meta.resources",
[UIFeature.StatsPanel] = "meta.stats",
[UIFeature.PortraitPanel] = "meta.portrait",
[UIFeature.ChatPanel] = "meta.chat",
[UIFeature.FullLayout] = "meta.layout",
[UIFeature.KeyboardShortcuts] = "meta.shortcuts",
[UIFeature.BoxAnimation] = "meta.animation",
[UIFeature.CraftingPanel] = "meta.crafting",
[UIFeature.CompletionTracker] = "meta.completion",
[UIFeature.AutoSave] = "meta.autosave",
};
var missing = featureKeys
.Where(kv => !en.ContainsKey(kv.Value))
.Select(kv => $"{kv.Key} -> '{kv.Value}'")
.ToList();
Assert.Empty(missing);
// Also verify every UIFeature enum value has a mapping
var allFeatures = Enum.GetValues<UIFeature>();
var unmapped = allFeatures.Where(f => !featureKeys.ContainsKey(f)).ToList();
Assert.Empty(unmapped);
}
[Fact]
public void AllMetaUnlockItems_ReferenceValidUIFeatures()
{
var items = LoadItems();
var metaItems = items.Where(i => i.MetaUnlock.HasValue).ToList();
// Every meta item's MetaUnlock value should be a valid UIFeature
// (this is guaranteed by deserialization, but let's verify the round-trip)
Assert.All(metaItems, item =>
Assert.True(Enum.IsDefined(item.MetaUnlock!.Value),
$"Item {item.Id} has invalid metaUnlock value"));
}
[Fact]
public void EnAndFr_HaveIdenticalKeysets()
{
var en = LoadEnStrings();
var frJson = File.ReadAllText(FrStringsPath);
var fr = JsonSerializer.Deserialize<Dictionary<string, string>>(frJson)!;
var onlyInEn = en.Keys.Where(k => !fr.ContainsKey(k)).ToList();
var onlyInFr = fr.Keys.Where(k => !en.ContainsKey(k)).ToList();
Assert.Empty(onlyInEn);
Assert.Empty(onlyInFr);
}
// ── ContentRegistry integration ────────────────────────────────────── // ── ContentRegistry integration ──────────────────────────────────────
[Fact] [Fact]