Source code for cardioception.PAT.languages

# Copyright (C) 2020-2026 Micah G Allen and the Embodied Computation Group, Aarhus University
"""Participant-facing text of the Phase Adjustment Task.

The instructions follow Hodgetts et al. (2026): move the beeps with the dial
until they fall on the heartbeat, explore freely, press space when they match,
then rate confidence from 1 (not at all) to 10 (extremely). Sit still, breathe
normally, do not tense the muscles, and attend to one place in the body. In the
exteroceptive task a vibration on the hand takes the place of the heartbeat.

The first live run showed that "turn the dial" was not enough: participants
need to hear that the beeps never change speed, that the wheel shifts each beep
within the beat, and which wheel that is.

The strings live in ``texts/<language>.yaml`` beside this module, in the
format of the HRD's (:mod:`cardioception.HRD.languages`), and are loaded and
checked by the same code, :mod:`cardioception._texts`. Three rules are the
PAT's own:

- **Device and exteroception** are ``variants``, as for the HRD: the rating
  help names the wheel or the mouse (axis ``device``), and every text that
  names the heartbeat names the vibration in the exteroceptive task (axis
  ``exteroception``).
- **The ends of the confidence scale** are written ``{low}`` and ``{high}``
  and replaced by the ends of the session's scale, as ``f"{end:g}"``. Only
  those two names are replaced, by plain substitution rather than
  ``str.format``, so other braces are left alone: ``tutorialProgress`` keeps
  its ``{done}`` and ``{total}`` for the task to fill in on every frame.
- **The dial page.** The ``instructions`` page that reads ``{dial}`` is
  replaced by ``dialPage`` when the dial is shown and by ``noDialPage`` when
  it is not. Neither key is in the returned dictionary.

The two languages were converted from the Python functions that held them
string for string, and ``tests/data/pat_language_snapshot.json`` holds what
those functions returned for every device, exteroception, dial and scale, so
the conversion is checked.

To add a language, copy ``texts/english.yaml``, translate the values, and pass
its filename stem as ``language``. Keys and placeholders are not translated.
"""

import os
from typing import Any, Dict, List

from .. import _texts

TEXTS_DIR = os.path.join(os.path.dirname(__file__), "texts")

#: Every key a language must define. Checked at load, so a language that is
#: missing one fails at launch rather than at the screen that needed it.
REQUIRED_KEYS = frozenset(
    {
        "Confidence",
        "VASlabels",
        "dialPage",
        "done",
        "instructions",
        "noDialPage",
        "ratingHelp",
        "textBreaks",
        "textNext",
        "textNextTrial",
        "textPracticeStart",
        "textTaskStart",
        "tooLate",
        "tutorialEarlier",
        "tutorialEnd",
        "tutorialFree",
        "tutorialIntro",
        "tutorialLater",
        "tutorialListen",
        "tutorialProgress",
        "tutorialReady",
        "waitingFirstTone",
    }
)

#: The instructions page replaced by the dial page, or by the page without it.
DIAL_PAGE = "{dial}"


[docs]def available() -> List[str]: """Languages that can be passed as ``language``.""" return _texts.available(TEXTS_DIR)
def _fill(value: Any, low: str, high: str) -> Any: """Put the scale ends in place of ``{low}`` and ``{high}``, and nothing else.""" if isinstance(value, list): return [_fill(v, low, high) for v in value] if isinstance(value, str): return value.replace("{low}", low).replace("{high}", high) return value
[docs]def get_texts( language: str, device: str = "keyboard", extero: bool = False, low: float = 1, high: float = 10, showDial: bool = True, ) -> Dict[str, Any]: """Every string this session will show the participant. Parameters ---------- language : A filename stem in ``texts/``. :func:`available` lists them. device : The confidence rating device, ``"keyboard"`` (the wheel) or ``"mouse"``. extero : If ``True``, the texts of the exteroceptive task (vibration instead of heartbeat). low, high : Ends of the confidence scale, named in the instructions. showDial : If ``True``, the instructions describe the dial on the screen. Raises ------ ValueError If the language does not exist, or its file is missing a key. Both are raised at setup, before any window opens. """ texts = _texts.get_texts( language, texts_dir=TEXTS_DIR, required=REQUIRED_KEYS, device=device, exteroception=extero, ) texts = {k: _fill(v, f"{low:g}", f"{high:g}") for k, v in texts.items()} dial = texts.pop("dialPage") no_dial = texts.pop("noDialPage") page = dial if showDial else no_dial texts["instructions"] = [ page if text == DIAL_PAGE else text for text in texts["instructions"] ] return texts