
This guide builds on the [mouse guide](./06-mouse.md). The [setup guide](./00-setup.md) already installed the CSV file, and we declared the `TEXT_*` constants back in the [composition guide](./02-composition.md). Here we explain how the code reads that CSV, add a helper to keep the calls short, and replace our last hardcoded string.

## Principle

All player-visible text must come from a CSV file -- never hardcode strings in Ruby. The PSDK i18n system picks the column matching the game's current language:

- Each CSV file has a unique numeric identifier (`TEXT_FILE_ID`).
- Each row maps to an index: the first data row is ID 0, the next is ID 1, and so on.
- Empty cells fall back to the English (`en`) value, so an unfinished translation still shows English.
- You read a row with `ext_text(file_id, row_index)`.

## The CSV and its constants

The CSV lives at `Data/Text/Dialogs/311125.csv` (installed in the setup guide). Its filename matches `TEXT_FILE_ID = 311_125`. Each row maps to a `TEXT_*` constant from `001 Constants.rb`:

| Constant               | Row | English text           |
| ---------------------- | --- | ---------------------- |
| `TEXT_ENTER_CODE`      | 0   | Enter Code             |
| `TEXT_QUIT`            | 1   | Quit                   |
| `TEXT_TITLE`           | 2   | Mystery Gift           |
| `TEXT_PROMPT`          | 3   | Enter your gift code   |
| `TEXT_INVALID`         | 4   | Invalid code!          |
| `TEXT_ALREADY_CLAIMED` | 5   | Gift already claimed!  |
| `TEXT_RECEIVED`        | 6   | You received %s!       |
| `TEXT_NO_GIFTS`        | 7   | No gifts claimed yet   |
| `TEXT_CONFIRM`         | 8   | Claim this gift?       |
| `TEXT_GIFT_RECEIVED`   | 9   | Gift received!         |
| `TEXT_YES`             | 10  | Yes                    |
| `TEXT_NO`              | 11  | No                     |

We already use two of these: the Composition reads `ext_text(TEXT_FILE_ID, TEXT_TITLE)` for the title and `TEXT_NO_GIFTS` for the empty message. The rest are used as we build the dialogs.

## A helper to shorten the calls

Writing `ext_text(TEXT_FILE_ID, id)` everywhere is verbose. Add a private helper to the scene. Update `scripts/20 MysteryGift/003 GamePlay/001 Main.rb` -- add this method (the `include UI::MysteryGift` at the top of the class makes the `TEXT_*` constants available):

```ruby
# Shortcut to access Mystery Gift text
# @param id [Integer] the text row index
# @return [String]
def gift_text(id)
  return ext_text(TEXT_FILE_ID, id)
end
```

- `gift_text(TEXT_QUIT)` is much clearer than `ext_text(311_125, 1)`.
- It returns the text already in the game's current language.

The Composition is in the `UI::MysteryGift` module and calls `ext_text` directly; the scene uses this `gift_text` shortcut. Both reach the same CSV.

## Localizing the buttons

In the scene guide we hardcoded `'Quit'` in `button_texts`. Replace it with the CSV value. Update `button_texts` in `001 Main.rb`:

```ruby
# Return the button texts for the ctrl buttons [A, X, Y, B]
# @return [Array<String, nil>]
def button_texts
  return [nil, nil, nil, gift_text(TEXT_QUIT)]
end
```

- The B button now reads its label from the CSV: "Quit" in English, "Quitter" in French.
- The A button stays hidden (`nil`) for now. We reveal it -- with its `gift_text(TEXT_ENTER_CODE)` label -- in the [dialogs guide](./08-dialogs.md), at the same time as the `action_a` it triggers. (A visible ctrl button calls its action on click, so it must not appear before its action exists.)

## Text colors

PSDK uses an indexed color palette:

```ruby
add_text(0, 0, 200, 18, gift_text(TEXT_TITLE), color: 0)   # black
add_text(0, 0, 200, 18, gift_text(TEXT_TITLE), color: 1)   # blue
add_text(0, 0, 200, 18, gift_text(TEXT_TITLE), color: 2)   # red
add_text(0, 0, 200, 18, gift_text(TEXT_TITLE), color: 3)   # green
add_text(0, 0, 200, 18, gift_text(TEXT_TITLE), color: 9)   # grey
add_text(0, 0, 200, 18, gift_text(TEXT_TITLE), color: 10)  # white
```

- The `color:` parameter is an index into the font palette. Common ones: 0 black, 1 blue, 2 red, 3 green, 9 grey, 10 white.

For an exact RGB color not in the palette, omit `color:` and assign `fill_color`/`outline_color` directly:

```ruby
title = add_text(0, 0, 200, 18, gift_text(TEXT_TITLE))
title.fill_color = Color.new(0, 255, 136)
title.outline_color = Color.new(0, 85, 45)
```

- A common trick for the outline is to divide each fill channel by 3: a fill of `(0, 255, 136)` pairs with an outline of `(0, 85, 45)`.

## Dynamic text with format

When a row contains a format specifier (`%s` for a string, `%d` for an integer), Ruby's `format` fills it in:

```ruby
# "You received Rare Candy!" / "Vous avez recu Rare Candy !"
message = format(gift_text(TEXT_RECEIVED), gift_name)
```

- `gift_text(TEXT_RECEIVED)` returns "You received %s!"; `format(template, gift_name)` replaces `%s`. We use this in the dialogs guide to announce the claimed gift.
- The gift name comes from the database, which is not translated here.

## Try it

Switch the game to French (or check the B button): the bottom button now reads "Quitter" instead of a hardcoded "Quit", and the title stays localized.

## Conclusion

- All player-visible text goes through a CSV identified by `TEXT_FILE_ID`; read it with `ext_text(file_id, row_index)`.
- The `TEXT_*` constants (declared in `001 Constants.rb`) name the CSV rows -- no magic numbers.
- Add a `gift_text` helper in the scene to shorten `ext_text` calls; the Composition uses `ext_text` directly.
- Never hardcode player-facing strings -- we replaced the last one (`'Quit'`).
- Use indexed `color:` for palette colors, or `fill_color`/`outline_color` for exact RGB.
- Use `format` with `%s`/`%d` for parameterized text.
