# Écrire une compétence

> Démarrez une compétence Thalovant avec SkillKit, puis trouvez de l'aide sur les langues, les conversations, les tests, l'empaquetage et l'installation.

Source: https://docs.thalovant.com/fr/developers/writing-a-skill/

---
{/* Last Edit: Codex - 2026-10-05 - Motive: Align the French SkillKit guides with the tested 0.24.2 tutorial and public helpers. */}

  Aside,
  Steps,
} from "@astrojs/starlight/components";

Une compétence est un petit programme qui confie une tâche à votre assistant. Elle peut raconter une blague, répondre à une question ou animer un quiz.

**Vous écrivez votre première compétence? Suivez [Créer un quiz éducatif](/fr/developers/skillkit-tutorial/).** Vous y construirez une compétence fonctionnelle en trois courtes leçons, chacune avec un résultat à vérifier. [Ce que SkillKit vous offre](/fr/developers/skillkit-capabilities/) liste tous les outils sur une seule page.

Cette page est votre manuel. Démarrez un autre projet ci-dessous, ou servez-vous de la table des matières pour trouver de l'aide sur une fonctionnalité précise.

`thalovant-skillkit` fournit la recherche de ressources, des outils pour les messages, des vérifications, ainsi que des outils facultatifs pour l'état des conversations et les tests d'intégration. OVOS reste responsable de l'acheminement des intentions, de la parole, de l'activation des sessions et des événements planifiés.

Consultez la [référence de l'API SkillKit](https://github.com/thalovant/thalovant-skillkit/blob/main/docs/reference.md) pour les signatures complètes des outils et leurs valeurs de retour.

## Python et un terminal

Utilisez Python 3.11 ou une version plus récente, et un terminal. Vous pouvez construire et tester en local sans hub; utilisez un hub pour la vérification orale finale.

Créez un environnement virtuel dans votre répertoire de développement. Ces commandes shell visent Linux et macOS :

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --pre "thalovant-skillkit==0.24.2"
```

SkillKit prend en charge Python 3.10 et les versions plus récentes; le projet qu'il génère pour vous déclare Python 3.11 comme minimum. Sous Windows, activez l'environnement avec `.venv\Scripts\Activate.ps1` dans PowerShell.

Ces exemples utilisent SkillKit **0.24.2**. L’option `--pre` autorise les
préversions OVOS utilisées par ses outils de test.

Tester une ancienne version d'OVOS

Si vous testez délibérément Workshop 8, installez `ovos-workshop==8.0.0` et
`setuptools<81` ensemble dans un environnement distinct : son ancien gestionnaire de plugins
importe `pkg_resources`, que les versions récentes de setuptools ont retiré. La version actuelle d'OVOS n'a pas
besoin de cette contrainte héritée.

## Partir d'une compétence fonctionnelle

Une seule commande écrit une compétence complète qui réussit déjà ses propres vérifications et tests :

```bash
thalovant-skillkit new garden-watering
cd thalovant-skill-garden-watering
python -m pip install --pre -e ".[test]"
thalovant-skillkit check
python -m pytest
```

Voici les fichiers avec lesquels vous travaillerez :

```
thalovant-skill-garden-watering/
├── thalovant_skill_garden_watering/
│   ├── __init__.py                   the skill
│   ├── version.py
│   └── locale/
│       ├── supported.json            which languages you ship
│       ├── en-US/
│       │   ├── vocab/GardenWateringKeyword.voc
│       │   ├── dialog/garden.watering.dialog
│       │   └── skill.json
│       └── fr-FR/                    starter resources to translate
├── test/
│   ├── test_skill.py                 hears its keyword, ignores the rest
│   └── test_contract.py              locales complete, packaging sound
└── pyproject.toml                    entry point and package data, declared
```

Le résultat est une compétence de repli accompagné de quatre tests de comportement et d'un test de contrat sur les sources. Exécutez les commandes suivantes depuis ce répertoire de projet généré. La classe de base trouve `locale/` à côté du module de votre compétence.

**Vérifiez votre résultat :** pytest signale cinq tests réussis. Vous avez créé un programme et vérifié ses premières réponses. Il n'est pas encore relié à un haut-parleur.

## Personnalisez-le

Les chemins qui commencent par `locale/` ci-dessous sont relatifs à `thalovant_skill_garden_watering/`, le répertoire du paquet importable.

1. **Indiquez à quoi votre compétence répond.**

   Un fichier `.voc` est une liste de mots ou d'expressions, un par ligne. `locale/en-US/vocab/GardenWateringKeyword.voc` :

   ```
   garden watering
   water the garden
   sprinklers
   ```

2. **Indiquez comment il répond.**

   Un fichier `.dialog` est une liste de réponses possibles, une par ligne. La sélection aléatoire peut répéter une ligne. `locale/en-US/dialog/garden.watering.dialog` :

   ```
   This garden skill is a demo; it has no watering schedule yet.
   I can hear garden questions, but I have no watering data yet.
   ```

3. **Écrivez la réponse.**

   `thalovant_skill_garden_watering/__init__.py` :

   ```python
   from thalovant_skillkit.skill import ThalovantFallbackSkill

   class GardenWateringSkill(ThalovantFallbackSkill):
       FALLBACK_PRIORITY = 98
       QUESTIONS_ONLY = True

       def can_answer(self, message) -> bool:
           text, lang = self.utterance(message), self.lang_of(message)
           return self.claims(text, lang) and self.mentions(text, "GardenWateringKeyword", lang)

       def reply(self, utterance, lang, context):
           return self.dialog("garden.watering", lang)
   ```

Cet exemple répond à des questions sur un sujet et prononce un texte de démonstration; il ne commande pas d'arroseurs et n'interroge aucun service. Remplacez la démonstration par votre propre logique de réponse quand vous serez prêt.

La classe de base de repli prononce un résultat `reply` non vide et renvoie `True`; un résultat vide renvoie `False` pour que le traitement de repli puisse continuer. `preview_reply` appelle la même logique de réponse sans émettre de parole. Des appels distincts peuvent choisir des lignes de dialogue différentes. Gardez `reply` exempt d'effets de bord sur le bus pour qu'il fonctionne dans les deux contextes.

`QUESTIONS_ONLY = True` et `claims()` empêchent les conversations ordinaires de la pièce de déclencher cette compétence de questions. Pour une commande comme « water the garden », utilisez plutôt une intention définie.

`can_answer` est consulté pendant la sélection du repli : gardez-le peu coûteux et étroit. Si vous redéfinissez `initialize`, appelez `super().initialize()` pour conserver l'enregistrement du repli.

## Choisir la bonne classe de base

| Votre interaction | Classe de base et implémentation |
| --- | --- |
| Une commande définie, comme « check the garden » | `ThalovantSkill`, un `@intent_handler` d'OVOS et une ressource `.intent`. Le gestionnaire prononce explicitement la réponse ou exécute l'action demandée. |
| Un sujet que les pipelines précédents n'ont pas traité | `ThalovantFallbackSkill` avec `can_answer` et `reply`, comme ci-dessus. Redéfinissez `handle_fallback` seulement quand renvoyer un texte parlé ne suffit pas. |
| Une relance qui dépend d'un tour précédent | `ThalovantConversationalSkill` avec un gestionnaire initial, `can_converse` et `converse`. L'activation et l'acheminement restent du ressort d'OVOS. |
| Recherche et lecture de médias par OCP | `ThalovantCommonPlaySkill` quand la classe de base facultative de compétence common-play d'OVOS est installée. Sans cette dépendance, elle n'est pas disponible (`None`). |

Les gestionnaires d'intentions n'appellent pas `reply` automatiquement. Par exemple, une compétence à intentions appelle explicitement `self.speak_dialog("garden.watering")` dans son gestionnaire décoré. Une compétence de repli peut aussi enregistrer des intentions pour ses commandes précises.

Une compétence qui répond sur un sujet et pose aussi des questions de relance peut utiliser
`ThalovantConversationalFallbackSkill`. Pour des médias avec des tours de relance, utilisez
`ThalovantConversationalCommonPlaySkill`. SkillKit compose pour vous les classes natives
d'OVOS, y compris leur enregistrement conversationnel.

## Laisser SkillKit gérer le travail répétitif

Consultez [Garder votre compétence réactive](/fr/developers/skillkit-runtime/) quand vous ajoutez un
service, une bibliothèque de sons, une actualisation en arrière-plan ou un état sauvegardé facultatif. Ce guide
indique quel outil choisir, comment vérifier le résultat et quand les caches s'actualisent.
News utilise les outils partagés de réseau et de tâches de fond; Fart utilise l'inventaire de sons partagé
et les outils de dialogues variés; Alarm, Timer, Reminder et Stopwatch partagent la persistance.
Votre compétence garde la partie intéressante : ce qu'elle fait et ce qu'elle dit.

## Choisir une priorité de repli

Dans la plage de repli basse, les numéros plus petits sont essayés en premier. Utilisez cette convention pour les compétences Thalovant :

| Priorité | Signification                 |
| -------- | ----------------------------- |
| 91–95    | Je réponds à une chose précise |
| 96–98    | Je réponds sur un sujet       |
| 99–100   | Je répondrai à presque tout   |

  Une revendication trop large peut empêcher un repli plus précis de répondre. Testez
  des demandes sans rapport avant de changer la priorité; changer la priorité ne peut pas corriger
  une implémentation de `can_answer` trop large.

Un opérateur peut remplacer `fallback_priority` par un entier de 91 à 100. Les réglages invalides ou hors plage utilisent la valeur par défaut de la classe; ils ne déclenchent pas d'erreur de configuration. La vérification des sources signale une valeur par défaut de classe hors plage.

## Ce que vous pouvez appeler

|                                    |                                                   |
| ---------------------------------- | ------------------------------------------------- |
| `self.utterance(msg)`              | ce qui a été dit                                  |
| `self.lang_of(msg)`                | la langue de cet énoncé                           |
| `self.location_of(msg)`            | le dictionnaire de localisation du message, ou `None` |
| `self.mentions(text, "Voc", lang)` | si le texte mentionne ce vocabulaire              |
| `self.mentioned_term(text, "Voc", lang)` | le terme correspondant, ou une chaîne vide  |
| `self.dialog("name", lang)`        | une ligne de `locale/<lang>/dialog/name.dialog`   |
| `self.speak_to(msg, text)`         | le dire à la pièce qui a demandé, dans sa langue  |
| `self.setting("key", default)`     | un réglage de la compétence                               |
| `self.locale_resources`            | toute l'arborescence `locale/`, si vous en avez besoin directement |

Tout ce que propose la classe de compétence d'OVOS fonctionne toujours : `self.speak`, `self.speak_dialog`, `self.voc_match`, `self.settings` et les décorateurs d'intentions. Utilisez `self.speak_to` quand la réponse doit atteindre le message que vous avez reçu : `converse()`, un crochet d'arrêt, ou un hub qui répond à deux pièces dans deux langues. `self.speak` trouve un message en parcourant la pile d'appels et parle dans la langue propre à la compétence. Préférez `self.speak_to(message, text)` pour répondre aux commandes reçues : une demande multilingue garde ainsi sa propre langue.

  `self.voc_match` appartient à OVOS et compare des mots entiers : un fichier qui liste `log`
  ne correspond donc pas à « logs ». `self.mentions` accepte aussi une courte terminaison : « logs »
  compte, alors que « technology » ne compte toujours pas. Utilisez `mentions` pour les mots-clés que les gens
  fléchissent, et `voc_match` quand vous voulez le mot exact. Aucun des deux tests n'exige que
  l'énoncé entier soit égal au terme : une relance comme « again » a besoin d'une
  vérification distincte sur l'énoncé entier pour éviter de revendiquer « set that timer again ».

## Écrire des intentions lisibles par OVOS

Un fichier `.intent` décrit une commande, une phrase par ligne. Commencez par
des phrases complètes, comme `démarre pocket quiz`. Ajoutez des gabarits seulement
s’ils rendent le fichier plus facile à lire :

```text
[démarre|lance] pocket quiz
(joue à|démarre) pocket quiz
```

Les parenthèses imposent un choix. Les crochets rendent toute la partie
facultative : la première ligne accepte donc aussi `pocket quiz`. Placez les
choix entre crochets ou parenthèses; `démarre|lance pocket quiz` est invalide.
`` insère les entrées de `vocab/QuizName.voc`, tandis que `{topic}` est
un emplacement dont le moteur d’intentions fournit la valeur à votre méthode.
Gardez les mêmes noms d’emplacements dans chaque traduction.
`thalovant-skillkit check` développe les gabarits selon les règles d’OVOS et
signale les lignes mal formées.

`locale_resources.matches_literal_intent(text, name, lang)` est plus précis :
il compare des **lignes littérales complètes**, sans tenir compte de la casse,
des accents ni de la ponctuation. Il ne développe pas les gabarits et n’extrait
pas les emplacements. Utilisez-le pour des commandes fixes dans une conversation;
laissez le moteur d’intentions traiter les gabarits. Le quiz garde des phrases
littérales pour que le replay et la commande vocale reconnaissent les mêmes mots.

Un fichier `.voc` contient des mots ou des motifs OVOS, pas des enregistrements
arbitraires. Si votre code lit des lignes comme `blue|bleu marine|bleu ciel`,
placez-les dans `locale/fr-FR/tables/colors.table` et chargez-les avec
`self.locale_resources.combined_lines(lang, "tables", "colors.table")`.
Ainsi, le lecteur de vocabulaire OVOS ne tente pas d’interpréter vos tables.

### Lire les autres transcriptions

La reconnaissance vocale peut proposer plusieurs possibilités.
`utterance(message)` retourne la première; `utterances(message)` les retourne
toutes, sans doublons :

```python
from thalovant_skillkit.message import utterances

for candidate in utterances(message):
    if self.locale_resources.matches_literal_intent(candidate, "pocket_quiz.start", lang):
        return self._say(message, "start")
```

Ici, `lang` est la langue du message reçu et `_say` est la méthode de réponse
du quiz. Acceptez la première commande valide, puis arrêtez la recherche. Une
autre transcription ne doit jamais produire une deuxième réponse ni modifier
le score une deuxième fois.

## Ajouter des pauses et guider la prononciation

Gardez les mots dans un fichier `.dialog` ordinaire. À côté, ajoutez un fichier
`.ssml` facultatif avec le même nom, les mêmes mots, les mêmes espaces réservés
et le même nombre de lignes non vides. SSML permet de donner de petites
instructions à la voix, comme « fais une pause ici ».

`locale/fr-FR/dialog/ready.dialog` :

```text
Prêt, {name}? On joue.
```

`locale/fr-FR/dialog/ready.ssml` :

```xml
Prêt, {name}? <break time="250ms"/> On joue.
```

Faites passer les deux formes par SkillKit :

```python
text = self.dialog("ready", self.lang_of(message), {"name": "Sam"})
self.speak_to(message, text)
```

**Résultat attendu :** le texte affiché reste `Prêt, Sam? On joue.` Une voix qui
prend en charge SSML peut faire une pause d’un quart de seconde après la question.
Les autres voix reçoivent toujours les mots ordinaires. La réponse garde la
langue et la session de la personne qui a parlé.

L’extension est `.ssml`, **pas** `.ssml.dialog`. Traduisez chaque fichier jumeau
avec son dialogue; SkillKit n’emprunte pas le jumeau d’une autre langue régionale.
Gardez les lignes littérales, sauf les espaces réservés : des `(choix|variés)`
ou des `[mots facultatifs]` font utiliser à `speak_dialog` le lecteur OVOS
ordinaire, sans jumeau. `self.dialog` ne développe pas ces motifs.
Lancez `thalovant-skillkit check` pour repérer les mots ou espaces réservés qui
ne correspondent pas.

Pour composer une phrase en Python, utilisez les outils plutôt que d’assembler du XML :

```python
from thalovant_skillkit import ssml

text = ssml.say("Votre code est", ssml.digits("204"), ssml.pause("250ms"), "Prêt?")
self.speak_to(message, text)
```

Les autres outils incluent `spell`, `telephone`, `foreign`, `emphasis` et `sub`.
Ils retournent un objet `Speech`, une chaîne avec du texte ordinaire et du balisage
facultatif. `ssml.say` et `+` conservent les deux formes; les chaînes formatées
et le formatage ordinaire perdent le balisage. Passez les textes d’une personne
ou d’un service comme valeurs, jamais comme gabarit SSML : SkillKit protège ces
valeurs. `SkillResources.dialog` lit seulement le texte ordinaire; utilisez
`self.dialog` sur une compétence pour obtenir aussi son jumeau vocal.

## Ajouter une autre langue

Traduisez les ressources françaises du squelette, y compris son mot-clé initial. Pour une langue supplémentaire, copiez un dossier de langue complet, traduisez ses ressources et ajoutez l'étiquette de langue à `locale/supported.json` :

```
locale/
├── en-US/vocab/GardenWateringKeyword.voc
└── fr-FR/vocab/GardenWateringKeyword.voc
```

Pour les tests en français ci-dessous, ajoutez `arroses le jardin` dans `fr-FR/vocab/GardenWateringKeyword.voc`. Mettez `Cette compétence de jardin est une démo sans données d’arrosage.` dans `fr-FR/dialog/garden.watering.dialog`. Le mot-clé anglais que le squelette place dans le dossier français est un espace réservé à remplacer.

Passez `self.lang_of(message)` explicitement aux outils de ressources, comme le fait l'exemple. Sans cet argument, ils utilisent la langue de la compétence, qui peut différer de celle de la personne qui parle.

### Prendre en charge les variantes régionales

Vous n'avez pas besoin de copier les fichiers anglais dans `en-CA`, `en-GB` et `en-AU`. SkillKit
peut utiliser la traduction `en-US` existante pour ces locuteurs. Il en va de même pour
les autres langues : `fr-BE` peut utiliser `fr-FR`, et `pt-AO` peut utiliser `pt-PT`.

Voyez un dossier régional comme un petit ensemble de remplacements. SkillKit cherche d'abord
une correspondance régionale exacte, puis une étiquette parente, puis la traduction de référence
de la langue et les traductions régionales compatibles. Ce n'est qu'ensuite qu'il essaie la
langue par défaut configurée, normalement `en-US`. Un dialogue existant fournit ses propres lignes;
un fichier absent ou vide poursuit le long de cette chaîne. La correspondance du vocabulaire inclut
les termes régionaux compatibles avant d'essayer la langue par défaut.

Avec les fichiers français de l'étape précédente, ajoutez ce test à
`test/test_skill.py`, qui importe déjà `GardenWateringSkill` :

```python
from thalovant_skillkit.testing import message

def test_canadian_french_uses_french_resources():
    skill = GardenWateringSkill()
    request = message("quand est-ce que tu arroses le jardin", lang="fr-CA")
    lang = skill.lang_of(request)
    assert lang == "fr-CA"
    assert skill.locale_resources.lang(lang) == "fr-FR"
    assert skill.dialog("garden.watering", lang) == (
        "Cette compétence de jardin est une démo sans données d’arrosage."
    )
```

**Résultat attendu :** la demande en français canadien lit la réponse française, tandis que
sa langue reste `fr-CA`. Continuez de passer cette langue d'origine à la parole et aux
services; le nom du dossier de ressources ne choisit pas l'accent, les unités,
la localisation ni le fuseau horaire de la personne.

Si le français canadien a besoin d'une formulation différente, ajoutez seulement
`locale/fr-CA/dialog/garden.watering.dialog`. Ce fichier l'emporte désormais pour `fr-CA`;
les autres ressources françaises manquantes viennent toujours de `fr-FR`. Listez les traductions complètes
dans `locale/supported.json`, qui pilote les vérifications d'exhaustivité. Les remplacements régionaux
partiels peuvent rester hors de la liste; la recherche à l'exécution utilise les répertoires fournis.

Le repli réutilise une traduction; il n'en crée pas. Un système d'écriture différent
exige des ressources adaptées : le chinois simplifié seul n'assure pas la prise en charge du chinois
traditionnel. La disponibilité de la reconnaissance et des voix dépend aussi des plugins
ASR et TTS du hub. Testez les sessions régionales dans OVOS en plus de ces tests de ressources.

### Générer des ressources régionales complètes

Utilisez SkillKit **0.15.0 ou une version ultérieure** quand une traduction régionale doit aussi fonctionner avec
les intentions natives d'OVOS ou avec du code qui lit les fichiers directement. Gardez les mots communs dans
le dossier de la langue d'origine et n'écrivez que les différences dans
`locale/regional.json`, à l'intérieur du paquet de votre compétence :

```json
{
  "version": 1,
  "locales": {
    "fr-CA": {
      "source": "fr-FR",
      "overrides": {
        "dialog/garden.watering.dialog": "Cette démo peut parler de la fin de semaine.\n"
      }
    },
    "en-CA": {"source": "en-US", "overrides": {}}
  }
}
```

Chaque clé de remplacement est un chemin de fichier relatif à son dossier de langue. Sa valeur est le
**texte complet du fichier**, y compris les `\n` entre les lignes. Laissez intacts les espaces réservés comme
`{score}`, les clés JSON, les clés des tables personnalisées placées avant `|` et les noms de groupes
d'expressions régulières. Cette commande copie le texte commun; elle ne le traduit pas pour vous.

Depuis le dépôt de la compétence, exécutez :

```bash
thalovant-skillkit locales --write
thalovant-skillkit check --no-fleet
python -m pytest -q
```

**Résultat attendu :** `fr-CA` contient la réponse canadienne ainsi que toutes les autres ressources
françaises. `en-CA` contient les ressources anglaises communes. Les deux étiquettes figurent dans
`supported.json`, et le vérificateur indique que la compétence respecte ses contrats.
Le test de repli précédent sélectionne maintenant `fr-CA` et attend la nouvelle réponse canadienne;
mettez à jour ces deux assertions après avoir généré cet exemple.

Validez (commit) le manifeste, les dossiers générés et le `supported.json` mis à jour. Les modifications
futures se font dans la langue source ou dans le manifeste; exécutez ensuite `locales --write` de nouveau.
Ne modifiez pas les fichiers générés directement : un `check` ordinaire signale les fichiers périmés ou modifiés
à la main. `thalovant-skillkit locales` effectue la même vérification de fraîcheur
sans rien écrire. Si un fichier source est supprimé, l'outil nomme les fichiers générés obsolètes
pour que vous les supprimiez explicitement; il ne les supprime jamais en silence.

Les sources doivent être des langues de base existantes dans la même langue. L'enchaînement d'une région
générée à une autre n'est pas pris en charge. Un remplacement régional doit nommer un fichier
source existant et en préserver les espaces réservés. Pour ajouter une nouvelle ressource, placez-la d'abord dans la
langue de base. La validation complète relève toujours de `check` et de vos tests.
La génération a lieu pendant le développement, sans appel réseau ni travail supplémentaire quand
quelqu'un parle. Les fichiers générés sont des données de paquet ordinaires et exigent les mêmes
vérifications de wheel que les autres traductions.

Commencez par les variantes régionales courantes des langues que votre compétence prend déjà en charge :

| Traduction commune | Cibles régionales courantes |
| --- | --- |
| `en-US` | `en-CA`, `en-GB`, `en-AU`, `en-NZ` |
| `fr-FR` | `fr-CA`, `fr-BE`, `fr-CH` |
| `es-ES` | `es-MX`, `es-AR`, `es-CO`, `es-US` |
| `de-DE` | `de-AT`, `de-CH` |
| `pt-PT` | `pt-AO`, `pt-MZ` |
| `nl-NL`, `sv-SE` | `nl-BE`, `sv-FI`, respectivement |
| `zh-CN` | `zh-SG`; `zh-TW` exige une formulation en chinois traditionnel |

Conservez les traductions régionales existantes là où elles conviennent déjà. Une formulation identique
peut être héritée en toute honnêteté; des dossiers distincts ne signifient pas des révisions distinctes par des
locuteurs natifs. L'orthographe régionale de l'anglais, les expressions du français canadien, l'orthographe
de l'allemand suisse et la grammaire de l'espagnol argentin exigent une révision en contexte. Pour le chinois,
[la configuration Taïwan d'OpenCC](https://github.com/BYVoid/OpenCC#configurations-配置文件)
peut aider à préparer les caractères traditionnels et les termes taïwanais courants. Vérifiez aussi les phrases
parlées et les littéraux des expressions régulières; la conversion ne crée pas de traduction cantonaise.
Demandez à des locuteurs qui maîtrisent la langue de réviser les parcours importants avant d'affirmer une qualité linguistique.

Les règles linguistiques communes proviennent de
[`thalovant-languages`](https://github.com/thalovant/thalovant-languages).
Utilisez la version **0.3.1 ou ultérieure** pour les recherches de règles en chinois traditionnel (`zh-TW`, `zh-HK`,
`zh-MO`, `zh-Hant`) et la détection des questions dans un texte chinois sans espaces :

```python
from thalovant_languages import asks

assert asks("什麼時候出發", "zh-TW")  # “When do we leave?” — no question mark needed.
```

SkillKit gère les dialogues traduits de votre compétence; `thalovant-languages` fournit
des listes de mots et des règles de grammaire communes. Vous n'avez pas besoin de copier ces règles dans
chaque dossier régional. La conversion d'écriture chinoise a lieu quand les responsables construisent
les données : les personnes qui utilisent la compétence n'ont donc pas besoin d'OpenCC à l'exécution.

```bash
python -m pip install --upgrade "thalovant-languages>=0.3.1"
thalovant-languages check
```

La vérification ne devrait signaler aucune erreur de données. Les versions arrivent d'abord sur
[PyPI](https://pypi.org/project/thalovant-languages/);
[piwheels](https://www.piwheels.org/project/thalovant-languages/) peut suivre plus tard.

Une compétence de repli qui publie des exemples `.intent` concrets peut accepter ces phrases
complètes avant de vérifier les mots-clés :

```python
def can_answer(self, message):
    text, lang = self.utterance(message), self.lang_of(message)
    return self.locale_resources.matches_literal_intent(
        text, "garden.watering", lang
    ) or (self.claims(text, lang) and self.mentions(text, "GardenWateringKeyword", lang))
```

Utilisez le nom d'une ressource d'intention qui existe réellement dans votre compétence. Cet outil normalise
la casse, les accents, la ponctuation et les espaces, lit les ressources régionales compatibles et
ignore les lignes qui contiennent des variables (slots) ou des motifs de rechange. Il ne remplace pas le
moteur d'intentions. Testez les phrases d'exemple de chaque langue déclarée et vérifiez que
les commandes sans rapport sont toujours laissées aux autres compétences.

## Rattacher les relances à la bonne conversation

Utilisez `ThalovantConversationalSkill` quand une réponse courte comme « again » dépend d'un tour précédent. Créez un `SessionStateStore` dans l'instance de la compétence pour conserver ce contexte :

```python
from thalovant_skillkit import session_of
from thalovant_skillkit.sessions import SessionStateStore
from thalovant_skillkit.skill import ThalovantConversationalSkill

class GardenConversationSkill(ThalovantConversationalSkill):
    def __init__(self, *args, **kwargs):
        self.topics: SessionStateStore[str] = SessionStateStore(
            max_entries=128, default_ttl=90
        )
        super().__init__(*args, **kwargs)

    def remember_topic(self, message, topic):
        session_id = session_of(message).get("session_id")
        if isinstance(session_id, str) and session_id:
            self.topics.set(session_id, topic, ttl=90)

    def can_stop(self, message):
        session_id = session_of(message).get("session_id")
        return (
            isinstance(session_id, str)
            and bool(session_id)
            and session_id in self.topics
        )

    def stop_session(self, session):
        self.topics.remove(session.session_id)
        # OVOS falls through to global stop() if this returns False.
        return True

    def stop(self):
        self.topics.clear()
        return True
```

Il s'agit d'une ébauche de gestion d'état, pas d'un remplacement complet de la compétence de repli. Ajoutez le gestionnaire d'intention initial et les deux méthodes de conversation avant d'utiliser la classe de base conversationnelle :

1. Le gestionnaire initial appelle `remember_topic`, prononce son invite et active la compétence par OVOS quand il a besoin de relances.
2. `can_converse` vérifie qu'il existe un état actif appartenant à la session entrante et une réponse pertinente, localisée, portant sur l'énoncé entier.
3. `converse` revérifie ces conditions avant de faire quoi que ce soit, car un arrêt ou une expiration a pu survenir depuis la sélection. Renvoyez `False` pour les tours sans rapport ou expirés. Ne renouvelez le TTL qu'après un tour accepté.

La fenêtre d'activation d'OVOS et le TTL de ce magasin sont distincts. Un statut actif ne prouve pas qu'un sujet est toujours en cours, et stocker un sujet n'active pas la compétence.

Le magasin suit ces règles :

- Les lectures par `get()` ne renouvellent pas l'expiration. Appelez `set()` quand un tour accepté doit la renouveler.
- `default_ttl=None`, ou `set(..., ttl=None)`, conserve une entrée jusqu'à sa suppression ou son éviction faute de capacité. Un magasin plein évince l'écriture la plus ancienne.
- L'expiration utilise une horloge monotone et survient quand on accède au magasin. Elle ne démarre aucune minuterie et ne conserve rien d'un redémarrage à l'autre.
- `remove(session_id)` efface une conversation. `clear()` efface l'instance. `remove(session_id, expected=state)` ne supprime que ce même objet stocké, de sorte que le nettoyage ne peut pas supprimer un objet de remplacement.
- Tenez `store.lock` autour des lectures et des mises à jour composées de valeurs modifiables. Gardez les E/S bloquantes et les attentes de lecture audio hors de ce verrou.

Utilisez l'identifiant de session OVOS déjà résolu à partir du message entrant. L'absence d'identifiant devrait suivre une politique explicite de la compétence; l'exemple refuse de s'en souvenir. Conservez la session OVOS renvoyée lorsque vous envoyez le tour suivant, comme décrit dans [Sessions et contexte](/fr/developers/sessions-context/).

OVOS reste responsable de `stop_session`, `stop`, `schedule_event` et `cancel_scheduled_event`. L'arrêt de session de l'exemple réussit même si son sujet a déjà expiré, ce qui empêche OVOS de basculer vers un arrêt global qui effacerait les autres haut-parleurs. Son `can_stop` ne revendique qu'un sujet actif pour la session qui fait la demande.

Effacer la mémoire n'arrête pas la lecture et n'annule pas une minuterie. Annulez séparément le travail planifié et la sortie de cette session, si votre compétence en possède. Utilisez le planificateur d'OVOS pour le travail qui doit s'exécuter à une échéance; l'expiration paresseuse du magasin n'exécute aucun rappel.

## Varier les questions et les sons

Un choix aléatoire peut tomber deux fois sur le même son. `ShuffleBag` fonctionne comme un tirage
de cartes dans un paquet mélangé : on utilise chaque choix, puis on mélange de nouveau.

```python
from random import Random
from thalovant_skillkit.selection import ShuffleBag

sounds = ShuffleBag(["duck.ogg", "bear.ogg", "robot.ogg"], rng=Random(7))
first = sounds.draw()
second = sounds.draw()
assert first != second
```

**Vérifiez votre résultat :** les deux premiers noms de fichiers diffèrent. Cet outil ne fait que choisir un
nom de fichier; utilisez `play_audio()` d'OVOS pour faire jouer le fichier fourni. Créez le sac dans
l'instance de votre compétence pour qu'il se souvienne des choix précédents d'une demande à l'autre. Une graine
aléatoire fixe rend un test reproductible; omettez `rng` en utilisation normale.

Si un autre haut-parleur joue entre deux tours, utilisez `sounds.draw(avoid=last_heard)`
avec le dernier nom de fichier du haut-parleur qui fait la demande. Le sac évite ce son chaque fois
qu'un autre choix existe. Un sac d'un seul son renvoie toujours ce son. Si le
son évité est le seul qui reste dans un sac plus grand, le tirage commence un nouveau cycle.
Conservez le dernier choix de chaque haut-parleur dans votre état de session.

## Le tester

Mettez à jour les tests de comportement du squelette en même temps que votre nouveau vocabulaire et vos nouvelles réponses; conservez son test de contrat sur les sources, qui est distinct :

```python
from thalovant_skillkit.testing import message

from thalovant_skill_garden_watering import GardenWateringSkill

def test_it_hears_its_keyword():
    assert GardenWateringSkill().can_answer(message("when should I water the garden"))

def test_it_hears_it_in_french():
    assert GardenWateringSkill().can_answer(message("quand est-ce que tu arroses le jardin", lang="fr-FR"))

def test_it_ignores_what_is_not_its_business():
    skill = GardenWateringSkill()
    assert not skill.can_answer(message("set a timer for ten minutes"))
    assert not skill.can_answer(message("the sprinklers are noisy today"))

def test_it_answers_in_french():
    assert GardenWateringSkill().preview_reply("quand est-ce que tu arroses le jardin", "fr-FR") == (
        "Cette compétence de jardin est une démo sans données d’arrosage."
    )
```

`message()` construit un message de test avec le texte de l'énoncé, la langue et un contexte facultatif. Pour les réponses qui dépendent de la localisation, importez `MONTREAL` depuis le même module et passez `location=MONTREAL`. Testez aussi une localisation absente : `location_of` renvoie `None` et votre compétence décide comment répondre.

  Gardez le test « it ignores ». Une compétence qui répond trop est plus difficile à repérer
  qu'une compétence qui ne répond pas assez, parce que rien n'échoue : une autre compétence
  cesse seulement d'être entendue.

## Dire une date ou une heure des deux façons

Un rappel lu à voix haute devrait dire « tomorrow at nine forty a.m. »; le même rappel dans une application devrait afficher « May 24, 9:40 AM ». Les outils `moments` de SkillKit fournissent les deux, et `speak_with_written` envoie les deux : un haut-parleur fait entendre l'une et un écran affiche l'autre :

```python
from thalovant_skillkit.moments import (
    date_text, duration_text, speak_with_written, time_text, uses_24_hour_clock,
)

def handle_reminder(self, message, due, now):
    lang = self.lang_of(message)
    clock = uses_24_hour_clock(lang)
    spoken = f"Reminder set for {date_text(due, lang, now)} at {time_text(due, lang, clock)}."
    written = (f"Reminder set for {date_text(due, lang, now, written=True)} "
               f"at {time_text(due, lang, clock, written=True)}.")
    speak_with_written(self, spoken, message, lang, written=written)
```

`date_text` conserve « today » et « tomorrow » quand la date est aussi proche. `duration_text(seconds, lang)` dit « five minutes » et écrit « 5:00 ». La forme écrite voyage à côté de la forme parlée, et un appareil sans écran l'ignore. Pour une réponse construite à partir de plusieurs morceaux, `WrittenForms.say(spoken, written)` enregistre chaque paire au fil de la composition, et `WrittenForms.render(reply)` produit la phrase écrite en une seule passe.

## Interroger le service de connaissances

Learning Lounge, Safety Guide et Source Scout répondent à partir du service de connaissances Thalovant. Ils passent tous par un seul client partagé, pas par trois copies :

```python
from thalovant_skillkit.knowledge import POLICY_BLOCKED, POLICY_UNAVAILABLE, knowledge_reply
from thalovant_skillkit.service import request_headers

reply = knowledge_reply(
    service_url=self.setting("knowledge_url", ""),
    prompt=question, lang=lang, mode="answer", max_sources=3,
    augmentation="auto", age_policy_setting=self.setting("age_policy"),
    headers=request_headers("my-skill", "1.0.0", "my-skill/1.0.0"), timeout=4.0,
)
if reply.policy in (POLICY_BLOCKED, POLICY_UNAVAILABLE):
    # The hub's age policy spoke, or could not be evaluated: say so, do not
    # substitute a packaged answer.
    ...
elif reply.answer:
    self.speak_to(message, reply.answer)
```

`reply.provenance` indique si le texte cite une source ou s'il a été rédigé par un modèle de langage à partir des sources qu'il cite, afin qu'une compétence puisse le préciser à la personne qui écoute. Le client fait une seule requête et ne réessaie pas; validez les réglages que vous lui passez.

## Rendre les réponses du bus partagé à leur demandeur

Si deux pièces envoient des requêtes au même service, attendre seulement son sujet
de réponse peut donner la première réponse aux deux pièces. Utilisez l'outil d'attente corrélée de SkillKit
avec un nouvel identifiant que le fournisseur renvoie en écho :

```python
from uuid import uuid4
from ovos_bus_client.message import Message
from thalovant_skillkit.bus import wait_for_response

def ask_provider(bus, context):
    request_id = uuid4().hex
    request = Message("example.query", {"id": request_id}, context)
    return wait_for_response(
        bus, request, "example.answer",
        matches=lambda reply: reply.data.get("id") == request_id,
        timeout=3.0,
    )
```

Passez le contexte du message d'origine, y compris sa session. Vérifiez que le
fournisseur renvoie réellement l'identifiant en écho; cet outil ne peut pas modifier le protocole d'un
service distant. Validez les données renvoyées et prononcez une réponse d'indisponibilité
en cas d'expiration du délai ou d'échec du transport. L'écouteur est retiré à chaque sortie. L'échéance
finie inclut l'envoi, mais ne peut pas interrompre un transport qui bloque
dans `emit`. Testez des requêtes simultanées, des réponses sans rapport ou tardives, et le
nettoyage de l'écouteur après l'expiration du délai.

## Solliciter le vrai bus OVOS

Choisissez la limite de test qui correspond au comportement que vous devez démontrer :

| Test | Démontre | Ne fournit pas |
| --- | --- | --- |
| Appels directs et `testing.FakeBus` | Logique de réponse, sélection de la langue, émissions enregistrées | Répartition, acheminement ni planificateur actif |
| `testing_ovos.skill_harness` | Initialisation de la compétence, répartition par le bus en amont, événements planifiés natifs | Un pipeline d'énoncés |
| `managed_minicroft` et `capture_turn` | L'acheminement réel dans le pipeline et les résultats sur le bus d'une compétence installée | Reconnaissance par microphone, qualité TTS ni acoustique d'un vrai haut-parleur |

Pour les exemples d'intégration, installez la pile facultative dans votre environnement de développement :

```bash
python -m pip install --pre "thalovant-skillkit[testing]==0.24.2"
```

Cet extra inclut OvoScope et les dépendances de préversion OVOS prises en charge. Installez-le dans l'environnement de développement où vous exécutez les exemples d'intégration.

`skill_harness` construit la compétence avec le FakeBus de répartition en amont, un fichier de réglages temporaire et le service de planification natif. Il fait le ménage en cas de réussite comme d'échec :

```python
from thalovant_skillkit.testing import message
from thalovant_skillkit.testing_ovos import skill_harness

from thalovant_skill_garden_watering import GardenWateringSkill

def test_the_skill_emits_speech():
    with skill_harness(
        GardenWateringSkill,
        skill_id="thalovant-skill-garden-watering.thalovant",
        scheduler=False,
    ) as harness:
        spoken = []
        harness.bus.on("ovos.utterance.speak", spoken.append)
        source = message("when should I water the garden", lang="en-US")
        assert harness.skill.can_answer(source)
        assert harness.skill.handle_fallback(source)
        assert len(spoken) == 1
        assert spoken[0].data["utterance"] in {
            "This garden skill is a demo; it has no watering schedule yet.",
            "I can hear garden questions, but I have no watering data yet.",
        }
```

Le planificateur est activé par défaut, avec son fil de minuterie arrêté pour que les tests puissent piloter sa méthode publique `tick()`. Utilisez `scheduler_autostart=True` pour tester une vraie livraison différée. L'outil ne fournit pas de pipeline d'intentions; utilisez MiniCroft pour l'acheminement des énoncés.

## Tester un énoncé avec MiniCroft

Isolez les chemins XDG avant que pytest ne collecte les modules qui importent votre compétence. Par exemple, dans `test/end2end/conftest.py` :

```python
from contextlib import ExitStack

from thalovant_skillkit.testing_ovos import isolated_xdg

def pytest_configure(config):
    cleanup = ExitStack()
    config.add_cleanup(cleanup.close)
    cleanup.enter_context(isolated_xdg())
```

Chaque processus de travail obtient ses propres répertoires temporaires, et l'environnement d'origine est restauré ensuite. Le module d'outils diffère ses importations d'OVOS jusqu'à ce qu'elles soient nécessaires. Le plugin pytest d'OVOScope a peut-être déjà chargé OVOS; l'isolation de configuration propre à MiniCroft reste activée séparément.

Un test de bout en bout pour la compétence de repli peut utiliser le pipeline de repli en amont :

```python
from ovos_bus_client.message import Message
from ovos_bus_client.session import Session
from ovoscope import FALLBACK_PIPELINE
from thalovant_skillkit.testing_ovos import (
    capture_turn,
    deferred_capture_gc,
    managed_minicroft,
)

def test_garden_question_routes_and_speaks():
    skill_id = "thalovant-skill-garden-watering.thalovant"
    with deferred_capture_gc(), managed_minicroft(
        [skill_id], default_pipeline=FALLBACK_PIPELINE,
    ) as croft:
        session = Session("garden-test")
        session.lang = "fr-FR"
        source = Message(
            "recognizer_loop:utterance",
            {"utterances": ["quand est-ce que tu arroses le jardin"], "lang": "fr-FR"},
            {"session": session.serialize()},
        )
        turn = capture_turn(croft, source, timeout=30)
        assert any(
            message.data.get("skill_id") == skill_id
            for message in turn.of_type("ovos.intent.matched")
        )
        assert turn.spoken == [
            "Cette compétence de jardin est une démo sans données d’arrosage."
        ]
        assert turn.session.session_id == session.session_id
```

`capture_turn` échoue à l'expiration du délai et détache toujours ses écouteurs de capture. Son résultat contient `messages`, `spoken`, `of_type(topic)` et la dernière `session` du haut-parleur d'origine; reportez cette session dans un message de relance. La mise à jour en arrière-plan d'un autre haut-parleur ne peut pas la remplacer. Ajoutez des assertions pour la réponse, l'audio ou le résultat de service attendus : une capture terminée ne prouve pas à elle seule que la compétence a bien répondu.

`deferred_capture_gc` circonscrit un contournement connu d'un interblocage de finaliseur entre OVOScope 1.8.5a1 et pyee 12.1.1. Il préserve le réglage précédent du ramasse-miettes et ne touche pas aux autres versions. Utilisez-le autour de la prise en charge de MiniCroft quand vous testez cette combinaison.

Couvrez une demande sans rapport, une conversation arrêtée ou expirée, et deux haut-parleurs qui utilisent la compétence indépendamment. Exécutez explicitement le répertoire de bout en bout si votre configuration pytest habituelle ne sélectionne que les tests unitaires :

```bash
python -m pytest test/end2end --timeout=300 --timeout-method=thread
```

## Vérifier le son et l'acheminement de Stop

Une capture peut contenir des messages de plusieurs haut-parleurs. Utilisez `for_session()` avant
de vérifier la parole, le son ou le Stop d'un haut-parleur. Cette méthode ne garde que les messages qui
portent explicitement cet identifiant de session; les messages qui n'en ont pas sont écartés.

Par exemple, un test de quiz peut vérifier les octets WAV réels mis en file pour un
haut-parleur distant nommé. `expected_file` est le chemin du son de réussite fourni avec le quiz :

```python
def assert_quiz_sound(turn, session_id, expected_file):
    mine = turn.for_session(session_id)
    assert len(mine.audio) == 1
    sound = mine.audio[0]
    assert sound.extension == "wav"
    assert sound.binary == expected_file.read_bytes()
```

Appelez ceci après avoir capturé le tour de bonne réponse du quiz. **Vérifiez votre résultat :**
le test échoue si ce haut-parleur n'a pas reçu le son attendu, si le mauvais
fichier a été mis en file ou si l'extension était incorrecte. Vérifiez aussi les autres sessions quand vous
devez exclure des copies en trop. La lecture locale peut plutôt porter un `sound.uri`, avec
`sound.binary` et `sound.extension` tous deux à `None`. L'outil n'ouvre jamais d'URI et ne
fait jamais jouer d'audio. Il vérifie les requêtes mises en file, pas ce qui est sorti du haut-parleur.

Pour un Stop capturé depuis la pièce A, vérifiez
`turn.for_session("room-a").audio_stops` et assurez-vous que
`turn.for_session("room-b").audio_stops` est vide. Envoyez ensuite un autre tour depuis
la pièce B et vérifiez sa réponse. Cette dernière étape confirme que l'arrêt de A a préservé
la partie de B, en plus de vérifier la destination du message Stop.

`spoken`, `audio` et `audio_stops` privilégient les noms de sujets actuels d'OVOS plutôt que leurs
miroirs hérités. Utilisez `of_type(topic)` quand vous devez inspecter un sujet précis.
Consultez la [référence des tests audio](https://github.com/thalovant/thalovant-skillkit/blob/main/docs/testing-audio.md)
pour les règles complètes sur les données et le filtrage.

## Le vérifier

```bash
thalovant-skillkit check
```

La vérification signale les ressources de langue manquantes, le JSON ou les expressions régulières mal formés dans les ressources, les priorités de classe invalides, les problèmes de point d'entrée et les déclarations d'empaquetage manquantes.

Les vérifications de langue comparent les langues prises en charge avec l'inventaire des fichiers `en-US`. Les vérifications des espaces réservés comparent les ensembles fichier par fichier. Les ressources françaises, y compris les régions générées à partir de `fr-FR`, conservent l'exemption existante de la comparaison avec l'anglais. Les remplacements régionaux doivent quand même préserver les espaces réservés de leur propre fichier source. Ces vérifications ne contrôlent pas chaque ligne traduite. Testez les réponses produites pour chaque langue que vous livrez, y compris le français. Un dialogue manquant renvoie le nom de sa ressource, et des valeurs de mise en forme manquantes peuvent laisser du texte de gabarit dans la réponse au lieu de lever une erreur.

Pour les fichiers `.intent`, SkillKit vérifie aussi si une autre compétence pourrait répondre aux mêmes phrases. Lisez les résultats : un avertissement sur une vérification de phrases ignorée signifie que cette partie n'a pas été vérifiée. Essayez des exemples plus précis si vos phrases chevauchent celles d'une autre compétence.

## Vérifier ce que vous avez construit

Construisez la wheel et l'archive source, puis comparez leur contenu à votre copie de travail. Remplacez les noms de fichiers d'exemple par ceux produits pour votre version :

```bash
python -m pip install build
python -m build
thalovant-skillkit check-artifacts . \
  --wheel dist/thalovant_skill_garden_watering-0.1.0-py3-none-any.whl \
  --sdist dist/thalovant_skill_garden_watering-0.1.0.tar.gz
```

Le vérificateur lit les archives sans les extraire ni importer la compétence. Il découvre le premier paquet `thalovant_skill_*` de premier niveau et vérifie ses fichiers octet par octet. Pour une structure `src/`, fournissez `--package src/your_package`; la découverte automatique ne descend pas dans `src/`.

Les ressources manquantes ou modifiées, les membres d'archive dangereux ou en double et les métadonnées mal formées font échouer la vérification. Quand les deux archives sont fournies, leurs noms de paquet et leurs versions doivent concorder. Cette vérification ne compare pas leur version déclarée avec celle de votre copie de travail; vérifiez ces métadonnées de version séparément.

Déclarez des règles d'inventaire supplémentaires quand votre compétence comporte des ressources d'exécution ou des générateurs hors ligne :

| Option | Ce qu'elle vérifie |
| --- | --- |
| `--package src/your_package` | Sélectionne explicitement un répertoire de paquet importable; répétez-la pour plusieurs paquets. |
| `--runtime path` | Exige un fichier ou un répertoire source supplémentaire dans les deux archives. |
| `--source scripts` | Exige les sources des générateurs dans l'archive source. |
| `--wheel-exclude scripts` | Garde les sources de ces générateurs hors de la wheel d'exécution. |
| `--wheel-count 'your_package/sounds/*.ogg=24'` | Exige un nombre exact de fichiers correspondants; `--sdist-count` vérifie l'archive source. |

Ces options décrivent les fichiers que votre projet livre réellement. Le décodage audio, l'intensité sonore et les vérifications d'écoute restent la responsabilité de la compétence audio.

## Vérifier le plugin installé

Le squelette a déjà déclaré le point d'entrée et les données du paquet dans `pyproject.toml` :

```toml
[project.entry-points."opm.skill"]
"thalovant-skill-garden-watering.thalovant" = "thalovant_skill_garden_watering:GardenWateringSkill"

[tool.setuptools.package-data]
thalovant_skill_garden_watering = ["locale/*.json", "locale/*/*", "locale/*/*/*"]
```

Le point d'entrée permet à OVOS de trouver votre classe. Les données du paquet incluent les ressources dont elle a besoin après l'installation. La vérification des sources inspecte ces déclarations; seule la vérification du paquet construit établit que les fichiers sont bien arrivés.

Depuis le répertoire du projet, installez la wheel dans un environnement distinct. Utilisez un chemin absolu vers la wheel et sortez de votre copie de travail avant l'importation, pour que Python ne puisse pas charger votre arborescence source par accident :

```bash
skill_wheel="$(pwd)/dist/thalovant_skill_garden_watering-0.1.0-py3-none-any.whl"
skill_smoke_dir="$(mktemp -d)"
python -m venv "$skill_smoke_dir/venv"
"$skill_smoke_dir/venv/bin/python" -m pip install --pre "$skill_wheel"
cd "$skill_smoke_dir"
"$skill_smoke_dir/venv/bin/python" - <<'PY'
from importlib.metadata import entry_points
from importlib.resources import files

plugin, = entry_points(
    group="opm.skill", name="thalovant-skill-garden-watering.thalovant"
)
skill_class = plugin.load()
assert skill_class.__name__ == "GardenWateringSkill"
dialog = files("thalovant_skill_garden_watering").joinpath(
    "locale/fr-FR/dialog/garden.watering.dialog"
)
assert "démo" in dialog.read_text(encoding="utf-8")
print("Installed plugin and French resources verified.")
PY
cd -
```

Charger le point d'entrée prouve que la classe installée peut être importée. Cela ne démarre pas de hub; utilisez le test MiniCroft pour l'acheminement et un vrai haut-parleur pour la vérification d'écoute finale.

## Préparer une version

Exécutez les vérifications des sources, les tests de comportement et les vérifications du paquet construit décrits ci-dessus après votre dernière modification de code ou de ressources. Testez les langues que vous prenez en charge, les relances, les signaux sonores et Stop.

Pour entendre votre compétence sur un haut-parleur, suivez [Ajouter votre propre compétence](/fr/manage/add-your-own-skill/) et [Ajouter une compétence à un hub](/fr/manage/add-a-skill-to-a-hub/). Posez les mêmes questions que dans vos tests, puis essayez une demande sans rapport. Si votre compétence poursuit une conversation, vérifiez que chaque haut-parleur conserve son propre état.

## Terminé quand

- `can_answer` renvoie vrai pour ce que votre compétence prend en charge et faux pour le reste;
- vos tests réussissent, y compris un pour une langue que vous prenez en charge et un pour une question que vous ne traitez pas;
- la wheel et l'archive source construites réussissent `check-artifacts`;
- le hub répond quand vous posez la question à voix haute.

## Ce que le symptôme signifie habituellement

| Ce que vous voyez                       | Ce que c'est habituellement                                                               |
| --------------------------------------- | ----------------------------------------------------------------------------------------- |
| La réponse est le nom du fichier de dialogue | Le fichier `.dialog` est absent, ou `package_data` a exclu `locale/` de l'installation |
| Répond en anglais à une question en français | Passez explicitement la langue du message; vérifiez la langue sélectionnée et son fichier de dialogue traduit. |
| Une autre compétence répond à la place        | Examinez d'abord l'acheminement et `can_answer`. Dans la plage de repli, un numéro de priorité plus petit s'exécute plus tôt. |
| Votre compétence répond à ce qui ne le concerne pas | `can_answer` est trop large : vérifiez ce que `mentions` reconnaît                  |
| Rien ne répond                          | Le nom du point d'entrée ne correspond pas à la classe, ou le paquet n'est pas installé sur le hub |
| Une réponse contient `{name}`           | Une valeur de gabarit est absente; testez les réponses produites avec les données que fournit chaque gestionnaire. |
| Une conversation expirée répond encore  | Les deux méthodes de conversation doivent vérifier l'état actuel de la session; l'activation par OVOS ne suffit pas. |
| Arrêter un haut-parleur en efface un autre | Limitez l'état et le travail planifié à la session; un résultat faux de `stop_session` bascule vers le `stop` global. |
| MiniCroft ne trouve pas la compétence        | Installez le projet dans cet interpréteur de test et vérifiez le nom exact de son point d'entrée `opm.skill`. |
| Une capture expire                      | Lisez les sujets des messages capturés et vérifiez le pipeline, l'état de préparation et les signaux de fin avant d'augmenter le délai. |
| Les vérifications des sources réussissent, mais il manque des ressources sur le hub | Reconstruisez, exécutez `check-artifacts` et importez la wheel depuis l'extérieur de votre copie de travail. |
