Skip to content

Tutorials · Interfaces · Chapter 13

Two languages

70 minutes Uses: LocalizedText, Dropdown, Tooltip, Modal, set_language, language, saved_text, save_text, set_text, pressed, amount

By the end

  • Two string tables, English and French, sixty-six keys each
  • A key on every word the game shows; counts filled in with {count}
  • A Language list in the settings; the choice survives a relaunch
On this page

String tables

Until now every word on a screen was typed onto the node that shows it. Two languages need the words in one place, apart from the nodes. A string table is a file of keys and the words they stand for: locales/en.ron holds English, the source language, and locales/fr.ron French. A node holds a key, bag.name, never the word, and the game looks the key up in the language it is in. Change the language and every word changes at once, and no scene holds a language.

  1. Choose Language → Edit tables…. The Locale tables window opens. It is empty, and says No string tables.
  2. Type en into the Locale box and press New locale…. The editor writes locales/en.ron at once, and it is the source language, because English is. Type fr and press New locale… again.
  3. Type a key into the Key box, title.play for example, and press Add key or Enter. The key is added to the source column only, so the French column now owes it. The Language menu’s hover on fr counts what is owed: it says how many keys will fall back to the source language.
  4. Type the English into a key’s en cell and the French into its fr cell. The recipe at the end of the chapter lists all sixty-six keys The Delve uses, by screen. For tip.empty and journal.empty, leave both cells empty: an empty cell is a word that says nothing, which is not a missing key.
  5. Press Save. It is copper while there are changes to write. It rewrites both files, sorted by key, and it is not on the undo stack: it saves files, not scene edits.
The Locale tables window open over the Viewport: tabs Strings (2) and Assets (0), a Save button, a Key box with the hint menu.resume and an Add key button, a Locale box with the hint de and a New locale… button, and under them a grid of keys from bag.drop to inspect.ok, each with an en cell and an fr cell, such as Drop and Jeter, and Empty the bag? and Vider le sac ?
KeyEnglishFrench
bag.nameBagSac
bag.empty_titleEmpty the bag?Vider le sac ?
hud.coins_tipCoins found: {count}.Pièces trouvées : {count}.
item.elixirElixirÉlixir
journal.hurtThe spikes hurt.Les pics ont fait mal.
settings.languageLanguageLangue
tip.emptyemptyempty

A key is a name, and renaming it is dangerous: a node that holds the old name finds nothing. So a key’s name in the grid renames it in every language at once when you click it, and its bin removes it from every language at once. Neither is written until you press Save. The window’s Assets tab holds a picture or a sound for each language, which The Delve does not use.

A key on every word

A Localized Text is the component that looks a key up. It does not draw. It writes the words into the node it sits on, at the end of every step, wherever they differ from what is there: its Key into the node’s Text or its dialog’s title, its Body into a dialog’s question, its Tooltip into the node’s Tooltip, its Placeholder into a field’s hint, and its Options into a list’s rows, a dialog’s answers or a menu’s items. Localized Text lists its fields.

  1. Open scenes/delve.ron. For each node in the table headed The Localized Texts: the delve, in the recipe, press Add component, choose LocalizedTextComponent and set the field the table names to the key it gives. The node’s own words stay as they are: they are what the editor shows until you preview a language.
  2. Do the same in scenes/settings.ron, scenes/slots.ron and scenes/title.ron, from the three tables that follow it.
  3. On coin_counter and gem_counter, add a row to Substitutions: Name count and Value 0.
bag_title selected, its Inspector narrowed with the search box, localized, to its Localized Text: Key bag.name, Body, Tooltip and Placeholder empty, Substitutions and Options each an empty list with a plus button; the Viewport shows the HUD, the journal window and the bag, whose title is outlined
Key
A Localized Text’s key into the string tables, for the node’s own words: its Text, or else its World Text, or else a dialog’s title. bag.name on the bag’s title. A key is a dotted name, screen.thing, so the table reads by screen. Empty, the Localized Text leaves the node’s words alone.
Body
A Localized Text’s key for the question of the dialog on its node: bag.empty_body on the bag’s question. Empty leaves the question as it was written.
Tooltip
A Localized Text’s key for its node’s Tooltip: hud.health_tip on the health bar. Empty leaves the tooltip as written.
Placeholder
A Localized Text’s key for the hint the Text Entry on its node shows while it is empty: slots.empty on each slot’s name field.
Substitutions, Name, Value
The values a Localized Text puts into the {name} places of the words it looks up. Each row is a Name, the word between the braces without them, and a Value, the text to put there. hud.coins_tip reads Coins found: {count}., so the coin counter has one row, Name count and Value 0, and a script keeps the Value current.
Options
A Localized Text’s keys for the rows of its node, in order: a list’s options, a dialog’s answers, or a menu’s items, a separator keeping its place. The bag slot’s menu takes menu.use, menu.drop and menu.inspect; the Empty bag question takes bag.keep and bag.empty. A row whose key is empty keeps the words it was given.

Counts in a sentence

  1. In Assets, press Create, choose New script, type counter_tip and press Enter. In the Code tab, replace everything in it with the script below and press Ctrl+S.
  2. Select coin_counter, press Add component and choose CounterTip. Its Kind is coins, the script’s own: leave it. Select gem_counter, add CounterTip and set its Kind to gems.
rgs counter_tip.rgs
/// The tooltip on a counter in the HUD: it fills in how many the hero has
/// found of what the counter counts, where the tooltip's words say {count}.
component CounterTip {
    /// The kept number it tells: coins or gems.
    kind: String = "coins",

    fn loop() {
        // The words come from the string tables, in the game's language, by
        // the Localized Text beside this script, whose first substitution,
        // named count, fills {count}. A pickup adds to its number on the step
        // the hero walks into it, and the tooltip says so from the next step.
        let found = kept_number(kind, 0.0);
        set_text("LocalizedTextComponent", "substitutions.0.value", text(found));
    }
}

The tooltip’s words come from the tables, in the game’s language, by the Localized Text beside this script. The script only keeps the number current: it reads the kept coins or gems, and writes it as text into the first substitution’s Value, which is named substitutions.0.value. A pickup adds to its number on the step the hero walks into it, so the tooltip says so from the next step.

The top left of the screen, close up: the HUD’s plate with the key counter reading 0 and the coin counter reading 5, and to the right of the coin counter a tooltip, PIÈCES TROUVÉES : 5., in the same brown frame and cream capitals
The coin counter’s tooltip in French, its count filled in.

Scripts that chose words now choose keys

Four scripts you already have wrote words. They now write keys, and the Localized Text beside each turns the key into the words of the language. Nothing about when they run changes.

  1. In the Code tab’s Scripts list, click inspect_dialog.rgs, replace everything in it with the script below and press Ctrl+S. Do the same for journal_line.rgs, load_slot.rgs and slot_tooltip.rgs.
rgs inspect_dialog.rgs
/// The dialog a bag slot's Inspect opens: it names the item kept under
/// inspect and says what it does, in the game's language, and OK, or Escape,
/// shuts it.
component InspectDialog {
    fn loop() {
        // A slot's Inspect keeps its item under inspect: 1 a potion, 2 the
        // elixir, 3 a key, and 0 once the dialog is shut.
        let item = kept_number("inspect", 0.0);
        // A click on OK, Enter, or Escape through the Cancel Action presses
        // inspect_ok on the game's next step.
        if pressed("inspect_ok") {
            keep_number("inspect", 0.0);
            hide();
        } else if item > 0.0 {
            // The item's name over what its tooltip says: the key of each,
            // which the Localized Text beside this script writes into the
            // dialog's title and its question in the game's language.
            if item == 1.0 {
                set_text("LocalizedTextComponent", "key", "item.potion");
                set_text("LocalizedTextComponent", "body", "tip.potion");
            } else if item == 2.0 {
                set_text("LocalizedTextComponent", "key", "item.elixir");
                set_text("LocalizedTextComponent", "body", "tip.elixir");
            } else {
                set_text("LocalizedTextComponent", "key", "item.key");
                set_text("LocalizedTextComponent", "body", "tip.key");
            }
            show();
        }
    }
}
rgs journal_line.rgs
/// A row of the journal's window: it shows one line of the journal in the
/// game's language, and hides while that line is not written yet.
component JournalLine {
    /// The line it shows, from 1 at the top to 20 at the bottom: the journal
    /// keeps it as journal_1 to journal_20.
    index: f32 = 1.0,

    fn loop() {
        // The keeper writes each line as the code of what happened, and 0
        // while the line is empty. Each code's words are a key in the string
        // tables, chosen here and only here, and the Localized Text beside
        // this script writes them into the row.
        let code = kept_number("journal_" + text(index), 0.0);
        let words = "journal.empty";
        if code == 1.0 {
            words = "journal.potion";
        } else if code == 2.0 {
            words = "journal.elixir";
        } else if code == 3.0 {
            words = "journal.key";
        } else if code == 4.0 {
            words = "journal.gem";
        } else if code == 5.0 {
            words = "journal.coin";
        } else if code == 6.0 {
            words = "journal.door";
        } else if code == 7.0 {
            words = "journal.drank";
        } else if code == 8.0 {
            words = "journal.hurt";
        } else if code == 9.0 {
            words = "journal.rest";
        }
        set_text("LocalizedTextComponent", "key", words);
        if words == "journal.empty" {
            // A hidden row takes no room in the journal's Stack, so the
            // journal is as long as the lines written.
            hide();
        } else {
            show();
        }
    }
}
rgs load_slot.rgs
/// A save slot's Start or Continue button, which opens the delve on the slot's
/// run. A slot in use carries on the run the shrine last saved in it. A free
/// slot starts a new game under the name typed in its field, by this button or
/// by Enter in the field, and starts nothing while the field is empty. The
/// game keeps each slot's saves under its own prefix: slot1. for the first,
/// slot2. and slot3. for the others.
component LoadSlot {
    /// The slot it opens: 1, 2 or 3.
    slot: f32 = 1.0,
    /// The health a new game starts with.
    start_health: f32 = 10.0,

    fn loop() {
        let saves = "slot" + text(slot) + ".";
        // Its slot's name field reports under slot_1_name, slot_2_name or
        // slot_3_name, and this button under slot_1_play and so on.
        let field = "slot_" + text(slot) + "_name";
        let name = saved_text(saves + "name", "");
        // Its word is the string tables' slots.start while the slot is free
        // and slots.continue once it is in use: this chooses the key, and the
        // Localized Text beside it writes the word in the game's language.
        if name == "" {
            set_text("LocalizedTextComponent", "key", "slots.start");
        } else {
            set_text("LocalizedTextComponent", "key", "slots.continue");
        }

        // A click on this button, or Enter while it holds the focus,
        // presses its action, and Enter in the name field presses the
        // field's, each on the game's next step. A free slot starts only once
        // a name is typed in its field.
        let typed = entered(field);
        let asked = pressed("slot_" + text(slot) + "_play") || pressed(field);
        if asked && (name != "" || typed != "") {
            if name == "" {
                // A new game: the slot is the name's from now on.
                save_text(saves + "name", typed);
            }
            // The slot's run, kept where the delve reads it. What the shrine
            // never saved reads as a new game's, and a free slot holds no
            // saves at all, since Delete forgets them: so a new game, or a
            // run that never rested at the shrine, starts at full health with
            // nothing carried, nothing in the journal, every pickup back in
            // the room and the door shut.
            keep_number("health", saved_number(saves + "health", start_health));
            keep_number("potions", saved_number(saves + "potions", 0.0));
            keep_number("elixirs", saved_number(saves + "elixirs", 0.0));
            keep_number("keys", saved_number(saves + "keys", 0.0));
            keep_number("gems", saved_number(saves + "gems", 0.0));
            keep_number("coins", saved_number(saves + "coins", 0.0));
            keep_number("haste", saved_number(saves + "haste", 0.0));
            for i in 1..5 {
                let kept = "hotbar_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            for i in 1..17 {
                let kept = "bag_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            for i in 1..21 {
                let kept = "journal_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            keep_number("journal_count", saved_number(saves + "journal_count", 0.0));
            // And the room as the run left it: each pickup taken, which stays
            // gone, and the door, which opens itself if it was opened.
            for i in 1..13 {
                let kept = "taken_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            keep_number("door_open", saved_number(saves + "door_open", 0.0));
            // No rest taken at the shrine yet, and nothing being inspected.
            keep_number("rests", 0.0);
            keep_number("inspect", 0.0);
            // The slot the run is played in, where the shrine saves it and
            // the way out marks it done.
            keep_number("slot", slot);
            // Loaded here, before the delve opens, so everything in it starts
            // on the slot's run: the hero on its health, and the journal's
            // keeper taking what the journal holds as already written.
            play_sound("click");
            change_scene("delve");
        }
    }
}
rgs slot_tooltip.rgs
/// The tooltip on a slot of the hotbar or the bag: it names the item the slot
/// holds, and says nothing while the slot is empty. It chooses the words' key
/// in the string tables, and the Localized Text beside it writes the words,
/// in the game's language, into the slot's own Tooltip component, which shows
/// them once the pointer has rested on the slot for its delay.
component SlotTooltip {
    /// The slot it speaks for, which is kept by that name: hotbar_1 to
    /// hotbar_4, or bag_1 to bag_16.
    slot: String = "hotbar_1",

    fn loop() {
        // The satchel and the slots keep what each slot holds: 1 a potion, 2
        // the elixir, 3 a key, 0 nothing.
        let item = kept_number(slot, 0.0);
        // An empty slot's key has no words in either language, and a Tooltip
        // with no words is never shown, however long the pointer rests.
        let words = "tip.empty";
        if item == 1.0 {
            words = "tip.potion";
        } else if item == 2.0 {
            words = "tip.elixir";
        } else if item == 3.0 {
            words = "tip.key";
        }
        set_text("LocalizedTextComponent", "tooltip", words);
    }
}

The journal still keeps a code for each line and not its words. Each row now picks a key from the code, journal.potion for code 1, and writes it into its Localized Text, so the line you read is in the language the game is in now, and the old lines change with it. Where the words are an empty cell, as for an empty slot’s tooltip, the Tooltip shows nothing, however long the pointer rests.

Room for French

  1. Set the sizes in the table below. French is longer than English: the same screens need wider frames and menus, and the settings screen has one more row, so its frame and title move up and out. One layout fits both languages.
  2. In scenes/settings.ron, set Down and Next of the Focus on text_size to language, and Up and Previous of the Focus on back to language, so the ring reaches the new list.
ScreenNodeFieldWasNow
The delvebag_1 to bag_16Pop-up Menu Width176200
key_hintsOffset Min-420, -44-500, -44
Offset Max420, -12500, -12
inspect_dialogOffset Min-300, -100-400, -100
Offset Max300, 100400, 100
pause_frameOffset Min-150, -45-170, -45
Offset Max150, 105170, 105
quit_dialogOffset Min-300, -53-360, -53
Offset Max300, 147360, 147
The settingssettings_nameOffset Min-360, -256-360, -286
Offset Max360, -176360, -206
settings_frameOffset Min-270, -160-330, -190
Offset Max270, 240330, 270
settings_rowsOffset Max-24, 312-24, 372
musicOffset Min244, -8324, -8
Offset Max484, 8604, 8
music_labelOffset Min-244, -24-324, -24
effectsOffset Min244, -8324, -8
Offset Max484, 8604, 8
effects_labelOffset Min-244, -24-324, -24
show_hintsOffset Min244, -16324, -16
Offset Max276, 16356, 16
show_hints_labelOffset Min-244, -24-324, -24
item_namesOffset Min244, -16324, -16
Offset Max276, 16356, 16
item_names_labelOffset Min-244, -24-324, -24
text_sizeOffset Min244, -20324, -20
Offset Max484, 20604, 20
text_size_labelOffset Min-244, -24-324, -24
The slotsslot_platesOffset Min-312, -126-342, -126
Offset Max312, 149342, 149
slot_1_name to slot_3_nameOffset Max336, 65376, 65
slot_1_play to slot_3_playOffset Min348, 16388, 16
Offset Max548, 65608, 65
slot_1_delete to slot_3_deleteOffset Min560, 16620, 16
Offset Max608, 65668, 65
delete_dialogOffset Min-320, -100-400, -100
Offset Max320, 100400, 100
The titletitle_frameOffset Min-130, 320-140, 320
Offset Max130, 531140, 531

Choosing the language

The settings screen gets a sixth row: a Language list. It is a Dropdown, built as the Text size list was, and its rows name each language in its own words, English and Français, so that a player who cannot read the language the game is in can still find their own.

  1. Open scenes/settings.ron. Select settings_rows, click + Add node and choose Node. Rename the node language_row. It comes after size_row.
  2. Select language_row, click + Add node and choose Dropdown. Rename the node language. In its Rect, set Anchor Min to 0, 0.5, Anchor Max to 0, 0.5, Offset Min to 324, -20, Offset Max to 604, 20 and Pivot to 0, 0.5. In its Panel, set Texture to input_rectangle.png and Border to 8. In its Text, set Text to Language, Scale to 0.75 and Align to Center.
  3. In its Dropdown, set Action to language, Options to two rows, English and Français, and Item Height to 40. Press Add component, choose FocusComponent, and set Up and Previous to text_size, and Down and Next to back. Press Add component again, choose LocalizedTextComponent and set its Key to settings.language.
  4. Select language, click + Add node and choose Panel. Rename the node language_arrow. In its Rect, set Anchor Min and Anchor Max both to 1, 0.5, Offset Min to -46, -11, Offset Max to -12, 11 and Pivot to 1, 0.5. Set its Panel’s Texture to icon_arrow_down_dark.png.
  5. Select language_row, click + Add node and choose Label. Rename the node language_label. In its Rect, set Anchor Min and Anchor Max both to 0, 0.5, Offset Min to -324, -24, Offset Max to -24, 24 and Pivot to 0, 0.5. In its Text, set Text to Language and Scale to 0.75. Add a LocalizedTextComponent with Key settings.language.
  6. Select settings_canvas and press Apply theme. It writes the colours of the new row: the list’s face and popup, 255, 255, 255, its lit row, 254, 231, 97, its pressed face, 211, 211, 211, its words, 63, 38, 49, and the label’s, 236, 227, 206.
  7. In Assets, press Create, choose New script, type language_list and press Enter. In the Code tab, replace everything in it with the script below and press Ctrl+S. Select language, press Add component and choose LanguageList.
rgs language_list.rgs
/// The Language list on the settings screen: it opens on the language the
/// game's text is in, and each time the player picks one it shows the whole
/// game in that language and saves the choice, which the title gives back as
/// the game opens. Its rows name each language in its own words, English and
/// then Français, the order of the codes it saves: en and fr.
component LanguageList {
    fn on_create() {
        // language() answers the code of the language the game's text is in.
        // Writing the list's choice changes what it shows and presses
        // nothing.
        if language() == "fr" {
            set_number("DropdownComponent", "selected", 1.0);
        } else {
            set_number("DropdownComponent", "selected", 0.0);
        }
    }

    fn loop() {
        // A click on a row, or Enter on the row the arrows lit, chooses it and
        // presses language on that step; amount() answers the row, from 0.
        if pressed("language") {
            let code = "en";
            if amount("language") == 1.0 {
                code = "fr";
            }
            // Every word in the game changes at the end of this step, and
            // the title asks for the saved language as the game next opens.
            set_language(code);
            save_text("language", code);
        }
    }
}
The Code panel on the settings scene with language_list.rgs open: a comment on the Language list, then component LanguageList with an on_create that asks language() and sets the list’s selected row, and a loop that reads pressed("language") and amount("language"); the Scripts list at the left names the project’s scripts, and the Hierarchy at the far left shows the Settings scene with its rows down to language_row

The list opens on the language the game’s text is in now: language answers its code, en or fr, and writing the list’s Selected presses nothing, since a script’s own write never does. After that the player chooses. pressed("language") is true on the step a real change is made: a click on a row, or Enter on the row the arrows lit, and amount("language") is the row’s number from 0, so 1 is French. A click on the row already chosen presses nothing.

set_language changes every word in the game at the end of that step: the last request in a step wins, and a request for the language already shown does nothing. save_text("language", code) keeps the choice in the game’s save file, so that the title can give it back.

  1. Open scenes/title.ron. In the Code tab’s Scripts list, click title.rgs, replace everything in it with the script below and press Ctrl+S.
rgs title.rgs
/// The title, as it opens: the language and the volumes the player chose in
/// Settings, set before the game's first word and its first sound, and the
/// menu faded in. The Canvas Group on this node starts at alpha 0, where the
/// menu is not drawn and takes no key; this brings it to 1 over half a second.
component Title {
    fn on_create() {
        // The language the player chose, which lands at the end of this
        // step, before the title is first drawn. With nothing saved it asks
        // for the language the game opened in, which changes nothing.
        set_language(saved_text("language", language()));
        // 0.8 until the player has chosen a volume.
        set_music_volume(saved_number("music_volume", 0.8));
        set_sfx_volume(saved_number("effects_volume", 0.8));
        anneal_number("CanvasGroupComponent", "alpha", 1.0, 0.5, .OutQuad);
    }
}

The title is the first scene of every launch, so it asks for the saved language before a word is drawn: saved_text("language", language()) is what was saved, or, if nothing was, the language the game opened in, which changes nothing. That is why the choice survives a relaunch.

The settings screen in French: PARAMÈTRES in gold, then MUSIQUE and EFFETS with their sliders, AFFICHER L’AIDE and NOMS DES OBJETS with tick boxes, TAILLE DU TEXTE with NORMALE, and LANGUE with its list open on FRANÇAIS, showing the rows ENGLISH and FRANÇAIS, the second lit yellow; the Retour button is partly hidden behind the open list
The settings in French, the list open.

Previewing

You do not have to play the game to see French. The editor can show it in the Viewport.

  1. Open scenes/delve.ron, and click Look through. Choose Language → fr.
  2. The Viewport’s labels read French: the bag’s title SAC, the Drop button JETER. The status line at the foot reads Previewing fr, and says either that every key is translated or how many fall back to the source language. Nothing is written into the scene: the previewed words are a lookup the editor does when it draws.
  3. Choose Language → Authored text to put the scene’s own words back.
The editor with the Language menu set to fr, its name shown on the toolbar: the Viewport shows the game camera’s picture with the bag’s title reading SAC and its button JETER, the HUD and the journal as before, and the status line at the foot reads Previewing fr - every key is translated
Language → fr: the Viewport’s labels in French.

Played in French, the delve reads French throughout: the journal’s lines, the bag, the hints. Below is the same walk, in French and then, for comparison, in English.

The delve in French: the HUD’s plate with its counters, the journal with six lines in French, Gemme trouvée., Clé trouvée., Les pics ont fait mal., Gemme trouvée., Potion trouvée. and Potion bue., the bag headed SAC with the key in its first slot and a red JETER button, and along the foot the hints line, WASD ou flèches marcher, B sac, J journal, 1-4 utiliser, Échap pause
The delve in French: the journal, the bag, the hints.
The same walk in English: the HUD’s plate, the journal with six lines, Found a gem., Found the key., The spikes hurt., Found a gem., Found a potion. and Drank a potion., the bag headed BAG with the key in its first slot and a red DROP button, and along the foot the hints line, WASD or arrows walk, B bag, J journal, 1-4 use, Esc pause
For comparison, the same walk in English.

A spreadsheet for a translator

You do not have to type another language yourself. A translator works in a spreadsheet, not in the editor: you hand them a file, they fill a column in, and they send it back. The Translator files window writes that file from the string tables, and reads it back.

  1. Choose Language → Translator files…. The Translator files window opens, in three bands.
  2. In Export to CSV, tick the languages to send, en and fr here, and press Export CSV…. Choose where to save, and keep the name locales.csv. The window says how many languages it wrote, and where. Send the file to your translator.
  3. When a file comes back, press Read CSV… in Merge a CSV and choose it. Nothing changes yet: the window shows what the file would do, one row for each language.
  4. Read the table. When it is what you expected, press the button that reads Merge and a number of cells; if it is not, press Discard.
  5. Choose Language → Edit tables… and press Save. The merge went into the tables only, and Save writes the files.
The Translator files window open over the Viewport, opened from the Language menu: three bands, Export to CSV, with its line One row per key, the source language first, the ticked boxes en and fr and an Export CSV… button; Merge a CSV, with its line about reading a returned file and a Read CSV… button; and Per-locale art and voice, which says No asset tables
Language → Translator files…: the window that hands the tables to a translator and takes them back.
Export to CSV
The band that writes the file that goes out: a first row naming the columns, key and then a language each, and one row for each key, in order. Every unsaved cell in the Locale tables window goes out with it, so you need not press Save first.
The language boxes
One box for each string table, named by its tag, and ticked as a language is added, since the reason to add one is to have it translated. The source language goes out first whatever you tick, so the translator has the English beside every cell to fill.
Export CSV…
Opens the system’s save dialog, named locales.csv, and writes the ticked languages. It is greyed, with Tick a language beside it, while no box is ticked.
Read CSV…
Opens the system’s file dialog for a file that came back, reads it, and counts what it would change. It writes nothing.
Language, Added, Updated, Unchanged
The table the window draws for a returned file, a row for each language in it. Added counts cells for keys the language had no words for, Updated cells whose words the file would change, and Unchanged cells that already hold what the file says. A language the project does not have yet is marked (new). Updated is coloured whenever it is above 0: a file that rewrites words nobody asked to revisit is an old export coming back over newer work.
The dropped keys line
Under the table, the keys in the file that the source table does not have, N key(s) the source language does not have, dropped, the first twelve named and the rest counted. It is usually a key renamed in English after the file went out. Send the list back to the translator, rather than adding the keys: the translation is for a word that no longer exists.
Merge … cell(s), Discard
The merge button names how many cells it will write, and writes exactly those into the tables. It reads Nothing to merge, and is greyed, when the file changes nothing. Discard throws the file’s plan away. Either way the window says what happened, and the plan is gone: pressing the merge twice would count every added cell as an update the second time.
Per-locale art and voice
The third band lists each language’s own pictures and sounds, by file name, for the person who repaints a sign or records a line. It only reads. The Delve has none, so it says No asset tables.

A file that is not a table the window can read says why, in red, and names the row as the spreadsheet counts it, with the header as row 1: a first column that is not key, a column header that is not a language, or a key that two rows share.

The string tables, the Localized Texts and the Language list

The string tables

ScreenKeyEnglishFrench
The HUDgame.nameThe DelveThe Delve
hud.coins_tipCoins found: {count}.Pièces trouvées : {count}.
hud.gems_tipGems found: {count}.Gemmes trouvées : {count}.
hud.health_tipHealth: spikes take it, potions give it back.Vie : les pics en prennent, les potions en rendent.
hud.hintsWASD or arrows walk / B bag / J journal / 1-4 use / Esc pauseWASD ou flèches marcher / B sac / J journal / 1-4 utiliser / Échap pause
hud.key_tipOpens the locked door, by walking into it.Ouvre la porte verrouillée quand on y entre.
The itemsitem.coinCoinPièce
item.elixirElixirÉlixir
item.gemGemGemme
item.keyKeyClé
item.potionPotionPotion
tip.elixirElixir: you walk faster for 5 seconds.Élixir : vous marchez plus vite pendant 5 secondes.
tip.emptyemptyempty
tip.keyKey: opens the locked door.Clé : ouvre la porte verrouillée.
tip.potionPotion: restores 3 health.Potion : rend 3 points de vie.
The bagbag.dropDropJeter
bag.emptyEmptyVider
bag.empty_bodyEverything in it is lost.Tout ce qu'il contient est perdu.
bag.empty_titleEmpty the bag?Vider le sac ?
bag.keepKeepGarder
bag.nameBagSac
menu.dropDropJeter
menu.inspectInspectInspecter
menu.useUseUtiliser
inspect.okOKOK
The journaljournal.coinFound a coin.Pièce trouvée.
journal.doorThe door opened.Porte ouverte.
journal.drankDrank a potion.Potion bue.
journal.elixirFound the elixir.Élixir trouvé.
journal.emptyemptyempty
journal.gemFound a gem.Gemme trouvée.
journal.hurtThe spikes hurt.Les pics ont fait mal.
journal.keyFound the key.Clé trouvée.
journal.nameJournalJournal
journal.potionFound a potion.Potion trouvée.
journal.restRested at the shrine.Repos au sanctuaire.
The pause menupause.leaveLeavePartir
pause.leave_bodyUnsaved progress is lost.La progression non sauvegardée est perdue.
pause.leave_titleLeave the delve?Quitter le donjon ?
pause.namePausedPause
pause.quitQuit to titleRetour au titre
pause.resumeResumeReprendre
pause.stayStayRester
The settingssettings.backBackRetour
settings.effectsEffectsEffets
settings.hintsShow hintsAfficher l'aide
settings.languageLanguageLangue
settings.largeLargeGrande
settings.musicMusicMusique
settings.nameSettingsParamètres
settings.namesItem namesNoms des objets
settings.normalNormalNormale
settings.smallSmallPetite
settings.text_sizeText sizeTaille du texte
The slotsslots.backBackRetour
slots.continueContinueContinuer
slots.deleteDeleteSupprimer
slots.delete_bodyIts name and progress are gone for good.Son nom et sa progression sont perdus pour de bon.
slots.delete_titleDelete this save?Supprimer cette sauvegarde ?
slots.emptyEmpty slotEmplacement vide
slots.keepKeepGarder
slots.nameSave slotsSauvegardes
slots.startStartCommencer
The titletitle.playPlayJouer
title.quitQuitQuitter
title.settingsSettingsParamètres

Sixty-six keys in each table. Every key of the English table is in the French one.

The Localized Texts: the delve

NodeComponentFieldValue
potion_1_name and potion_2_nameLocalized TextKeyitem.potion
elixir_nameLocalized TextKeyitem.elixir
key_nameLocalized TextKeyitem.key
gem_green_nameLocalized TextKeyitem.gem
gem_red_nameLocalized TextKeyitem.gem
gem_blue_nameLocalized TextKeyitem.gem
coin_1_name to coin_5_nameLocalized TextKeyitem.coin
hud_titleLocalized TextKeygame.name
health_barLocalized TextTooltiphud.health_tip
key_counterLocalized TextTooltiphud.key_tip
coin_counterLocalized TextTooltiphud.coins_tip
Substitutionsone row: Name count, Value 0
CounterTipKindcoins
gem_counterLocalized TextTooltiphud.gems_tip
Substitutionsone row: Name count, Value 0
CounterTipKindgems
bag_titleLocalized TextKeybag.name
bag_1 to bag_16Localized TextTooltiptip.empty
Optionsmenu.use, menu.drop, menu.inspect
drop_buttonLocalized TextKeybag.drop
key_hintsLocalized TextKeyhud.hints
hotbar_1 to hotbar_4Localized TextTooltiptip.empty
empty_dialogLocalized TextKeybag.empty_title
Bodybag.empty_body
Optionsbag.keep, bag.empty
inspect_dialogLocalized TextKeyitem.potion
Bodytip.potion
Optionsinspect.ok
journal_titleLocalized TextKeyjournal.name
journal_1 to journal_20Localized TextKeyjournal.empty
pause_nameLocalized TextKeypause.name
resumeLocalized TextKeypause.resume
quit_to_titleLocalized TextKeypause.quit
quit_dialogLocalized TextKeypause.leave_title
Bodypause.leave_body
Optionspause.stay, pause.leave

The Localized Texts: the settings

NodeComponentFieldValue
settings_nameLocalized TextKeysettings.name
music_labelLocalized TextKeysettings.music
effects_labelLocalized TextKeysettings.effects
show_hints_labelLocalized TextKeysettings.hints
item_names_labelLocalized TextKeysettings.names
text_sizeLocalized TextKeysettings.text_size
Optionssettings.small, settings.normal, settings.large
text_size_labelLocalized TextKeysettings.text_size
backLocalized TextKeysettings.back

The Localized Texts: the slots

NodeComponentFieldValue
slots_nameLocalized TextKeyslots.name
slot_1_name to slot_3_nameLocalized TextPlaceholderslots.empty
slot_1_play to slot_3_playLocalized TextKeyslots.start
slot_1_delete_label to slot_3_delete_labelLocalized TextKeyslots.delete
backLocalized TextKeyslots.back
delete_dialogLocalized TextKeyslots.delete_title
Bodyslots.delete_body
Optionsslots.keep, slots.delete

The Localized Texts: the title

NodeComponentFieldValue
title_nameLocalized TextKeygame.name
playLocalized TextKeytitle.play
settingsLocalized TextKeytitle.settings
quitLocalized TextKeytitle.quit

The Language list

NodeComponentFieldValue
languageRectAnchor Min0, 0.5
Anchor Max0, 0.5
Offset Min324, -20
Offset Max604, 20
Pivot0, 0.5
PanelColor255, 255, 255, alpha 255
Textureinput_rectangle.png
Border8
TextTextLanguage
Scale0.75
Color63, 38, 49, alpha 255
AlignCenter
DropdownActionlanguage
OptionsEnglish, Français
Item Height40
Normal255, 255, 255, alpha 255
Hovered254, 231, 97, alpha 255
Pressed211, 211, 211, alpha 255
Popup255, 255, 255, alpha 255
FocusUptext_size
Downback
Nextback
Previoustext_size
Localized TextKeysettings.language
language_arrowRectAnchor Min1, 0.5
Anchor Max1, 0.5
Offset Min-46, -11
Offset Max-12, 11
Pivot1, 0.5
PanelColor255, 255, 255, alpha 255
Textureicon_arrow_down_dark.png
language_labelRectAnchor Min0, 0.5
Anchor Max0, 0.5
Offset Min-324, -24
Offset Max-24, 24
Pivot0, 0.5
TextTextLanguage
Scale0.75
Color236, 227, 206, alpha 255
Localized TextKeysettings.language

language_row is a plain Node with no component of its own. The colours of the list, its arrow and its label are written by Apply theme. Every other field of every node is as it comes.

Check it works