Translate and word-wrap cutscene modal text
uiModalOpen now resolves title/message/option strings as locale message IDs first, falling back to the literal string if no match is found, and word-wraps the result to stay within the screen's scan width via the new shared textWrap helper. sceneinitial.c's no-device modal now uses locale keys instead of hardcoded English strings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,32 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||
|
||||
msgid "initial.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "initial.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "initial.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "initial.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "initial.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "initial.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
|
||||
@@ -187,3 +187,37 @@ int32_t textMeasure(
|
||||
|
||||
return spriteCount;
|
||||
}
|
||||
|
||||
void textWrap(char_t *text, const font_t *font, const float_t maxWidth) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
assertNotNull(font, "Font cannot be NULL");
|
||||
|
||||
float_t fontWidth = (float_t)font->tileset->tileWidth;
|
||||
if(fontWidth <= 0.0f) return;
|
||||
|
||||
int32_t charsPerLine = (int32_t)(maxWidth / fontWidth);
|
||||
if(charsPerLine <= 0) return;
|
||||
|
||||
int32_t lineWidth = 0;
|
||||
int32_t lastSpace = -1;
|
||||
|
||||
for(int32_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '\n') {
|
||||
lineWidth = 0;
|
||||
lastSpace = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(text[i] == ' ') {
|
||||
lastSpace = i;
|
||||
}
|
||||
|
||||
lineWidth++;
|
||||
|
||||
if(lineWidth > charsPerLine && lastSpace != -1) {
|
||||
text[lastSpace] = '\n';
|
||||
lineWidth = i - lastSpace;
|
||||
lastSpace = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,3 +104,17 @@ int32_t textMeasure(
|
||||
int32_t *outWidth,
|
||||
int32_t *outHeight
|
||||
);
|
||||
|
||||
/**
|
||||
* Word-wraps text in place for display at up to maxWidth pixels wide,
|
||||
* by replacing the space nearest each overflow point with a newline.
|
||||
* Length is unchanged - this only ever swaps existing spaces for
|
||||
* newlines, never inserts characters - so it's always safe to call on a
|
||||
* fixed-size buffer. A single word wider than maxWidth on its own is
|
||||
* left unbroken.
|
||||
*
|
||||
* @param text Null-terminated, caller-owned buffer to wrap in place.
|
||||
* @param font Font to measure character width with.
|
||||
* @param maxWidth Maximum line width, in pixels.
|
||||
*/
|
||||
void textWrap(char_t *text, const font_t *font, const float_t maxWidth);
|
||||
|
||||
@@ -89,6 +89,9 @@ typedef struct cutscene_s {
|
||||
// the dialog to be dismissed. Script the rest of the interaction (e.g.
|
||||
// CUTSCENE_CALLBACK to kick off work, CUTSCENE_WAIT, then
|
||||
// CUTSCENE_MODAL_CLOSE) as later items in the same cutscene.
|
||||
// TITLE and MESSAGE are each displayed as-is unless they match a
|
||||
// locale message ID, in which case the translated string is shown
|
||||
// instead - see uiModalLocalize.
|
||||
#define CUTSCENE_MODAL(TITLE, MESSAGE) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||
@@ -100,8 +103,10 @@ typedef struct cutscene_s {
|
||||
// CALLBACK fires with the selected option index (or UI_MODAL_RESULT_NONE
|
||||
// if backed out of) once the dialog closes. Option labels are passed as
|
||||
// trailing arguments, e.g. CUTSCENE_MODAL_OPTIONS(title, message,
|
||||
// callback, "Retry", "Cancel") - their strings are not copied, so they
|
||||
// must outlive the modal (string literals are fine).
|
||||
// callback, "Retry", "Cancel") - their strings are not copied by this
|
||||
// item, so they must stay valid until the modal opens (string literals
|
||||
// are fine). Like TITLE and MESSAGE, each option is translated if it
|
||||
// matches a locale message ID.
|
||||
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||
|
||||
@@ -28,12 +28,16 @@ typedef void (*cutscenemodaloptioncallback_t)(
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
// Display text, or a locale message ID to translate - see
|
||||
// uiModalLocalize.
|
||||
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
||||
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
||||
|
||||
// NOT copied - these pointers are stored directly by the underlying
|
||||
// buttons, so they must stay valid for as long as the modal is open
|
||||
// (e.g. string literals, as CUTSCENE_MODAL_OPTIONS produces).
|
||||
// NOT copied by this struct - these pointers are only read once,
|
||||
// while uiModalOpen is opening the modal in cutsceneModalStart, so
|
||||
// they only need to stay valid until then (e.g. string literals, as
|
||||
// CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a
|
||||
// locale message ID, same as title/message.
|
||||
const char_t **options;
|
||||
uint8_t optionCount;
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ void sceneInitialSaveDeviceRetryCallback(const uint8_t opt, void *u);
|
||||
|
||||
CUTSCENE(INITIAL, 0, DEFAULT,
|
||||
CUTSCENE_MODAL(
|
||||
"Checking for save data...",
|
||||
"Please wait."
|
||||
"initial.checking_save.title", "initial.checking_save.message"
|
||||
),
|
||||
CUTSCENE_WAIT(1.0f),
|
||||
CUTSCENE_CALLBACK(sceneInitialFindDevices),
|
||||
@@ -31,10 +30,10 @@ CUTSCENE(INITIAL, 0, DEFAULT,
|
||||
CUTSCENE_MARKER("NO_DEVICE"),
|
||||
CUTSCENE_MODAL_CLOSE(),
|
||||
CUTSCENE_MODAL_OPTIONS(
|
||||
"No save device found",
|
||||
"Please ensure a save device is connected and try again.",
|
||||
"initial.no_device.title",
|
||||
"initial.no_device.message",
|
||||
sceneInitialSaveDeviceRetryCallback,
|
||||
"Retry", "Continue without saving"
|
||||
"initial.no_device.retry", "initial.no_device.continue"
|
||||
),
|
||||
|
||||
CUTSCENE_MARKER("RETRY"),
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
#define UI_MODAL_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
||||
|
||||
@@ -119,6 +121,20 @@ errorret_t uiModalDraw(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiModalLocalize(
|
||||
const char_t *key,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize
|
||||
) {
|
||||
errorret_t result = assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale, key, 0, buffer, bufferSize
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(result);
|
||||
stringCopy(buffer, key, bufferSize - 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool_t uiModalIsOpen(void) {
|
||||
return UI_MODAL.open;
|
||||
}
|
||||
@@ -146,11 +162,16 @@ void uiModalOpen(
|
||||
optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal"
|
||||
);
|
||||
|
||||
stringCopy(UI_MODAL.titleText, title, UI_MODAL_TITLE_TEXT_MAX - 1);
|
||||
float_t maxContentWidth = ((float_t)SCREEN.scanWidth * UI_MODAL_MAX_WIDTH_RATIO)
|
||||
- (UI_FRAME_START_X * 2.0f);
|
||||
|
||||
uiModalLocalize(title, UI_MODAL.titleText, UI_MODAL_TITLE_TEXT_MAX);
|
||||
textWrap(UI_MODAL.titleText, &FONT_DEFAULT, maxContentWidth);
|
||||
UI_MODAL.titleLabel.dirty = true;
|
||||
uiLabelRebuffer(&UI_MODAL.titleLabel);
|
||||
|
||||
stringCopy(UI_MODAL.messageText, message, UI_MODAL_MESSAGE_TEXT_MAX - 1);
|
||||
uiModalLocalize(message, UI_MODAL.messageText, UI_MODAL_MESSAGE_TEXT_MAX);
|
||||
textWrap(UI_MODAL.messageText, &FONT_DEFAULT, maxContentWidth);
|
||||
UI_MODAL.messageLabel.dirty = true;
|
||||
uiLabelRebuffer(&UI_MODAL.messageLabel);
|
||||
|
||||
@@ -165,7 +186,10 @@ void uiModalOpen(
|
||||
&UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL
|
||||
);
|
||||
for(uint8_t i = 0; i < optionCount; i++) {
|
||||
MENU_BUTTON(options[i]);
|
||||
uiModalLocalize(
|
||||
options[i], UI_MODAL.optionsText[i], UI_MODAL_OPTION_TEXT_MAX
|
||||
);
|
||||
MENU_BUTTON(UI_MODAL.optionsText[i]);
|
||||
}
|
||||
MENU_END(UI_MODAL.options, menuIndex);
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
#define UI_MODAL_MESSAGE_TEXT_MAX 256
|
||||
#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX
|
||||
#define UI_MODAL_OPTIONS_MAX 4
|
||||
#define UI_MODAL_OPTION_TEXT_MAX 32
|
||||
#define UI_MODAL_MIN_WIDTH 160.0f
|
||||
// Title/message text wraps at word boundaries to stay within this
|
||||
// fraction of the screen's scan width, so long (e.g. translated)
|
||||
// strings don't run off-screen.
|
||||
#define UI_MODAL_MAX_WIDTH_RATIO 0.8f
|
||||
#define UI_MODAL_RESULT_NONE 0xFF
|
||||
|
||||
/**
|
||||
@@ -54,6 +59,7 @@ typedef struct {
|
||||
|
||||
uimenu_t menu;
|
||||
uimenuitem_t options[UI_MODAL_OPTIONS_MAX];
|
||||
char_t optionsText[UI_MODAL_OPTIONS_MAX][UI_MODAL_OPTION_TEXT_MAX];
|
||||
|
||||
uimodaloptioncallback_t callback;
|
||||
uimodalopenedcallback_t onOpen;
|
||||
@@ -99,14 +105,19 @@ uint8_t uiModalGetResult(void);
|
||||
* callback is invoked exactly once with the result, whether the dialog
|
||||
* was dismissed by selecting an option or by pressing cancel/back.
|
||||
*
|
||||
* @param title Display title; copied internally, safe to be transient.
|
||||
* @param message Display message; copied internally, safe to be
|
||||
* transient.
|
||||
* @param options Array of option label strings; NOT copied internally,
|
||||
* the pointers are stored directly by the underlying buttons, so they
|
||||
* must remain valid for as long as the modal is open (e.g. string
|
||||
* literals or locale-owned strings). May be NULL for a message-only
|
||||
* dialog with no option buttons, in which case optionCount must be 0.
|
||||
* title, message, and each option are treated as locale message IDs
|
||||
* first: if one matches an entry in the active locale file its
|
||||
* translated string is used, otherwise the string itself is displayed
|
||||
* verbatim. See uiModalLocalize.
|
||||
*
|
||||
* @param title Display title, or a locale message ID; copied
|
||||
* internally, safe to be transient.
|
||||
* @param message Display message, or a locale message ID; copied
|
||||
* internally, safe to be transient.
|
||||
* @param options Array of option label strings/locale message IDs;
|
||||
* copied internally, safe to be transient. May be NULL for a
|
||||
* message-only dialog with no option buttons, in which case optionCount
|
||||
* must be 0.
|
||||
* @param optionCount Number of options, from 0 to UI_MODAL_OPTIONS_MAX.
|
||||
* @param callback Called with the result once the dialog closes. May be
|
||||
* NULL.
|
||||
@@ -139,3 +150,18 @@ void uiModalClose(uimodalclosedcallback_t callback);
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiModalDispose(void);
|
||||
|
||||
/**
|
||||
* Resolves display text for uiModalOpen: if key matches a locale
|
||||
* message ID in the active locale file, buffer is filled with its
|
||||
* translated string, otherwise key is copied into buffer verbatim.
|
||||
*
|
||||
* @param key Display string or locale message ID.
|
||||
* @param buffer Destination buffer.
|
||||
* @param bufferSize Size of buffer, in bytes.
|
||||
*/
|
||||
void uiModalLocalize(
|
||||
const char_t *key,
|
||||
char_t *buffer,
|
||||
const size_t bufferSize
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user