Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
69aa9f0
Feat: Added a language option in setting menu. Added a little test tr…
CaesinEachonson Aug 9, 2026
1eb8568
Fix: Gobo format check
CaesinEachonson Aug 9, 2026
ed38ba3
Feature: extract hardcoded strings in chapter creation session to en.…
CaesinEachonson Aug 10, 2026
b90fbdb
fix(localization): address review comments (lookups, fonts, CM_get_fo…
CaesinEachonson Aug 10, 2026
fadab29
Fix: address review comments
CaesinEachonson Aug 10, 2026
ca0a8d5
Fix: crash on switching language
CaesinEachonson Aug 10, 2026
9af5a62
Merge feat/localization-base into feat/zh-chapter-creation
CaesinEachonson Aug 10, 2026
3489afd
chore(l10n): refine translation strings
CaesinEachonson Aug 10, 2026
501a658
chore(l10n): refine translation strings
CaesinEachonson Aug 10, 2026
72c45be
fix: potential crash on opening squad view
CaesinEachonson Aug 11, 2026
d453c7b
merge: solve conflicts with main branch
CaesinEachonson Aug 11, 2026
1b87c88
fix: address review comments
CaesinEachonson Aug 11, 2026
8b11e6d
fix: address review comment
CaesinEachonson Aug 11, 2026
b93317a
fix: address review comment
CaesinEachonson Aug 11, 2026
4d0f5a3
fix: address review comment
CaesinEachonson Aug 11, 2026
df7585a
fix: address review comment
CaesinEachonson Aug 11, 2026
3fb4445
fix: address review comment
CaesinEachonson Aug 11, 2026
0899bec
address review comment
CaesinEachonson Aug 11, 2026
4d4d5ae
fix: address review comment
CaesinEachonson Aug 11, 2026
96a3eb1
fix: address review comment
CaesinEachonson Aug 12, 2026
7afe752
fix: address review comment
CaesinEachonson Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
518 changes: 500 additions & 18 deletions datafiles/lang/en.json

Large diffs are not rendered by default.

489 changes: 471 additions & 18 deletions datafiles/lang/zh.json

Large diffs are not rendered by default.

152 changes: 76 additions & 76 deletions objects/obj_creation/Draw_0.gml

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions objects/obj_creation_popup/Draw_0.gml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ try {

} else if (type == ePOPUP_TYPE.EQUIP) {
draw_popup_equip(before_after_styling);
draw_set_font(fnt_40k_30b);
draw_set_font(cjk_font(fnt_40k_30b));

var _role_data = obj_creation.player_role_data[target_role];
var _role_name = _role_data.role;

role_name_input.tooltip = $"Astartes Role Name/nThe name of this Astartes Role. The plural form will be ''{_role_name}s''.";
role_name_input.tooltip = $"Astartes Role Name\nThe name of this Astartes Role. The plural form will be ''{_role_name}s''.";
role_name_input.draw(_role_name);
if (role_name_input.value_allowed){
_role_data.role = role_name_input.draw(_role_name);
Expand All @@ -21,15 +21,15 @@ try {

if ((tooltip != "") && (obj_creation.change_slide <= 0)) {
draw_set_alpha(1);
draw_set_font(fnt_40k_14);
draw_set_font(cjk_font(fnt_40k_14));
draw_set_halign(fa_left);
draw_set_color(0);
draw_rectangle(mouse_x + 18, mouse_y + 20, mouse_x + string_width_ext(string_hash_to_newline(tooltip2), -1, 500) + 24, mouse_y + 44 + string_height_ext(string_hash_to_newline(tooltip2), -1, 500), 0);
draw_set_color(CM_GREEN_COLOR);
draw_rectangle(mouse_x + 18, mouse_y + 20, mouse_x + string_width_ext(string_hash_to_newline(tooltip2), -1, 500) + 24, mouse_y + 44 + string_height_ext(string_hash_to_newline(tooltip2), -1, 500), 1);
draw_set_font(fnt_40k_14b);
draw_set_font(cjk_font(fnt_40k_14b));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: In Chinese (CJK) mode the tooltip/role-name popup now goes through cjk_font() for both the bold title (fnt_40k_14b) and the regular body (fnt_40k_14). LocalizationManager.get_font() caches its fallback font by point size only (string(_size)), and both of these fonts are size 14, so they resolve to the same cached CJK font and the bold/regular visual distinction disappears (and fnt_40k_14i would collapse into the same entry too). This makes the bold title text render with body weight in the localized UI. Consider including the style/typeface in the cache key (or explicitly documenting that CJK fallback can't preserve weight), so same-size fonts with different styles don't silently share one glyph set.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At objects/obj_creation_popup/Draw_0.gml, line 30:

<comment>In Chinese (CJK) mode the tooltip/role-name popup now goes through cjk_font() for both the bold title (fnt_40k_14b) and the regular body (fnt_40k_14). LocalizationManager.get_font() caches its fallback font by point size only (`string(_size)`), and both of these fonts are size 14, so they resolve to the same cached CJK font and the bold/regular visual distinction disappears (and fnt_40k_14i would collapse into the same entry too). This makes the bold title text render with body weight in the localized UI. Consider including the style/typeface in the cache key (or explicitly documenting that CJK fallback can't preserve weight), so same-size fonts with different styles don't silently share one glyph set.</comment>

<file context>
@@ -21,15 +21,15 @@ try {
         draw_set_color(CM_GREEN_COLOR);
         draw_rectangle(mouse_x + 18, mouse_y + 20, mouse_x + string_width_ext(string_hash_to_newline(tooltip2), -1, 500) + 24, mouse_y + 44 + string_height_ext(string_hash_to_newline(tooltip2), -1, 500), 1);
-        draw_set_font(fnt_40k_14b);
+        draw_set_font(cjk_font(fnt_40k_14b));
         draw_text(mouse_x + 22, mouse_y + 22, string_hash_to_newline(string(tooltip)));
-        draw_set_font(fnt_40k_14);
</file context>

draw_text(mouse_x + 22, mouse_y + 22, string_hash_to_newline(string(tooltip)));
draw_set_font(fnt_40k_14);
draw_set_font(cjk_font(fnt_40k_14));
draw_text_ext(mouse_x + 22, mouse_y + 42, string_hash_to_newline(string(tooltip2)), -1, 500);
}
} catch (ex) {
Expand Down
2 changes: 1 addition & 1 deletion objects/obj_star_select/Create_0.gml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ main_data_slate = new DataSlate();
potential_donors = [];

colonist_button = new PurchaseButton(1000);
colonist_button.update({tooltip: "Planets with higher populations can provide more recruits both for your chapter and to keep a planets PDF bolstered, however colonists from other planets bring with them their home planets influences and evils /n REQ : 1000", label: "Request Colonists", target: target});
colonist_button.update({tooltip: "Planets with higher populations can provide more recruits both for your chapter and to keep a planets PDF bolstered, however colonists from other planets bring with them their home planets influences and evils \nREQ : 1000", label: "Request Colonists", target: target});
colonist_button.bind_method = function() {
var doner = array_random_element(obj_star_select.potential_donors);
new_colony_fleet(doner[0], doner[1], target.id, obj_controller.selecting_planet, "bolster_population");
Expand Down
16 changes: 10 additions & 6 deletions objects/obj_tooltip/Draw_75.gml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ for (var i = 0; i < array_length(tooltip_data); i++) {
var _force_width = tooltip_data[i].force_width;
var _cost = tooltip_data[i].cost;

var _draw_font = cjk_font(_font);
var _draw_header_font = cjk_font(_header_font);
var _draw_footer_font = cjk_font(_footer_font);

var _screen_vpadding = 30;
var _screen_hpadding = 60;
var _header_h = 0;
Expand All @@ -33,18 +37,18 @@ for (var i = 0; i < array_length(tooltip_data); i++) {
var _rect_h = 0;

// Measurements
draw_set_font(_font);
draw_set_font(_draw_font);
var _text_w = _force_width ? _width : min(string_width(_tooltip), _width);
var _text_h = string_height_ext(_tooltip, DEFAULT_LINE_GAP, _text_w);

if (_header != "") {
draw_set_font(_header_font);
draw_set_font(_draw_header_font);
_header_w = _force_width ? _width : max(string_width(_header), _width);
_header_h = string_height_ext(_header, DEFAULT_LINE_GAP, _header_w);
}

if (_footer != "") {
draw_set_font(_footer_font);
draw_set_font(_draw_footer_font);
_footer_w = _force_width ? _width : max(string_width(_footer), _width);
_footer_h = string_height_ext(_footer, DEFAULT_LINE_GAP, _footer_w);
}
Expand Down Expand Up @@ -99,15 +103,15 @@ for (var i = 0; i < array_length(tooltip_data); i++) {
draw_rectangle_color_simple(_rect_x + 2, _rect_y + 2, _rect_w + _rect_x - 2, _rect_h + _rect_y - 2, 1, c_gray);

if (_header != "") {
draw_set_font(_header_font);
draw_set_font(_draw_header_font);
draw_text_ext_transformed_colour(_content_x, _header_y, _header, DEFAULT_LINE_GAP, _header_w, 1, 1, 0, _text_color, _text_color, _text_color, _text_color, 1);
}

draw_set_font(_font);
draw_set_font(_draw_font);
draw_text_ext_transformed_colour(_content_x, _text_y, _tooltip, DEFAULT_LINE_GAP, _text_w, 1, 1, 0, _text_color, _text_color, _text_color, _text_color, 1);

if (_footer != "") {
draw_set_font(_footer_font);
draw_set_font(_draw_footer_font);
draw_text_ext_transformed_colour(_content_x, _footer_y, _footer, DEFAULT_LINE_GAP, _footer_w, 1, 1, 0, _text_color, _text_color, _text_color, _text_color, 1);
if (_cost != 0) {
var _cost_color = (obj_controller.requisition < _cost) ? c_red : COL_REQUISITION;
Expand Down
191 changes: 187 additions & 4 deletions scripts/LocalizationManager/LocalizationManager.gml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ function LocalizationManager() constructor {
needs_cjk = false;
translations = {};
cjk_fonts = {};
font_styles = {};
// Keys already warned about as missing this language load, so draw-time arrays do not
// flood LOGGER.error every frame for the same gap. Reset on each language load.
reported_missing = {};

/// @param {string} _language Language code: LANG_EN, LANG_ZH, etc.
static load_language = function(_language) {
self.language = _language;
self.needs_cjk = _language != LANG_EN && string_count(LANG_ZH, _language) > 0;
self.translations = self._load_lang_file(_language);
self.reported_missing = {};
self._warn_missing_translations();
};

Expand Down Expand Up @@ -77,14 +82,99 @@ function LocalizationManager() constructor {
}

if (_args != undefined) {
for (var i = 0; i < array_length(_args); i++) {
_value = string_replace_all(_value, "{" + string(i) + "}", string(_args[i]));
var _count = array_length(_args);
var _tokens = [];
for (var i = 0; i < _count; i++) {
var _token = chr(2) + "LOC_ARG_" + string(i) + chr(2);
array_push(_tokens, _token);
_value = string_replace_all(_value, "{" + string(i) + "}", _token);
}
for (var i = 0; i < _count; i++) {
_value = string_replace_all(_value, _tokens[i], string(_args[i]));
}
}

return _value;
};

/// @desc Localizes every entry of an array in one call, returning a new array. Each element
/// is treated as an English translation key (or a { text, variables } struct for
/// keys with {0}, {1} placeholders). Missing keys fall back to the English value and
/// are reported with LOGGER.error so translation gaps surface loudly instead of
/// silently showing the wrong language.
/// @param {Array} _keys Array of English keys and/or { text, variables } structs.
/// @returns {Array}
static localize_array = function(_keys) {
var _result = array_create(array_length(_keys), "");
for (var i = 0; i < array_length(_keys); i++) {
var _item = _keys[i];
_result[i] = self._localize_item(_item);
}
return _result;
};

/// @desc Reports a missing translation key once per language load. Calling this from
/// _localize_item (which may run every draw frame for draw-time arrays) with an
/// un-deduplicated LOGGER.error would flood the log, so each key fires at most once
/// until the next load_language resets reported_missing.
/// @param {string} _key The untranslated English key.
static _report_missing = function(_key) {
if (struct_exists(self.reported_missing, _key)) {
return;
}
self.reported_missing[$ _key] = true;
LOGGER.error($"No translation for '{_key}' in language '{self.language}'.");
};

/// @desc Localizes a single array entry: an English key, or a { text, variables } struct.
/// Empty values pass through untouched; missing keys fall back to English with a
/// once-per-language-load LOGGER.error warning.
/// @param {string|Struct} _item English translation key or struct with placeholder data.
/// @returns {string}
static _localize_item = function(_item) {
if (is_struct(_item)) {
var _text = _item[$ LANG_ENTRY_TEXT];
var _variables = struct_exists(_item, LANG_ENTRY_VARIABLES) ? _item[$ LANG_ENTRY_VARIABLES] : undefined;
if (_text != "" && !struct_exists(self.translations, _text)) {
self._report_missing(_text);
}
return self.translate(_text, _variables);
}
if (!is_string(_item) || _item == "") {
return _item;
}
if (!struct_exists(self.translations, _item)) {
self._report_missing(_item);
}
return self.translate(_item);
};

/// @desc Rebuilds the localized global faction_names display array from the pristine English
/// source (global.faction_names_en), translating once per language change instead of on
/// every draw frame. Translating from the constant English source each time makes the
/// call fully idempotent and round-trip safe: switching to another language and back to
/// English always restores the original English names. Call from
/// SettingsManager.apply_language().
static refresh_locale_globals = function() {
for (var i = 0; i < array_length(global.faction_names_en); i++) {
global.faction_names[i] = self.translate(global.faction_names_en[i]);
}

// Rebuild each live rating array from its pristine English source, exactly like
// faction_names above. Writes go directly through each global accessor so the live
// array CoW-copies away from the shared _en source; translating from English each time
// keeps the call idempotent and round-trip safe.
for (var i = 0; i < array_length(global.chapter_strength_ratings_en); i++) {
global.chapter_strength_ratings[i] = self.translate(global.chapter_strength_ratings_en[i]);
}
for (var i = 0; i < array_length(global.chapter_cooperation_ratings_en); i++) {
global.chapter_cooperation_ratings[i] = self.translate(global.chapter_cooperation_ratings_en[i]);
}
for (var i = 0; i < array_length(global.chapter_geneseed_ratings_en); i++) {
global.chapter_geneseed_ratings[i] = self.translate(global.chapter_geneseed_ratings_en[i]);
}
};

/// @param {real} _size Point size used for the runtime fallback font.
/// @param {real} _base_font The font asset intended for this text.
/// @returns {real}
Expand All @@ -93,12 +183,13 @@ function LocalizationManager() constructor {
return _base_font;
}

var _key = string(_size);
var _style = self._get_font_style(_base_font);
var _key = string(_size) + ":" + string(_style.bold) + ":" + string(_style.italic);
if (struct_exists(self.cjk_fonts, _key)) {
return self.cjk_fonts[$ _key];
}

var _fallback_font = font_add(STR_CJK_FALLBACK_FONT, _size, false, false, 32, 65535);
var _fallback_font = font_add(STR_CJK_FALLBACK_FONT, _size, _style.bold, _style.italic, 32, 65535);
if (!font_exists(_fallback_font)) {
LOGGER.error($"Failed to load CJK fallback font '{STR_CJK_FALLBACK_FONT}' at size {_size}. Chinese glyphs may render as blank boxes.");
self.cjk_fonts[$ _key] = _base_font;
Expand All @@ -108,6 +199,71 @@ function LocalizationManager() constructor {
self.cjk_fonts[$ _key] = _fallback_font;
return _fallback_font;
};

/// @desc Returns a font's bold/italic style, cached per font asset so detection (and any
/// warnings) run once per session rather than on every draw call.
/// @param {real} _base_font The font asset intended for this text.
/// @returns {Struct}
static _get_font_style = function(_base_font) {
var _font_id = string(_base_font);
if (struct_exists(self.font_styles, _font_id)) {
return self.font_styles[$ _font_id];
}

var _style = self._font_style(_base_font);
self.font_styles[$ _font_id] = _style;
return _style;
};

/// @desc Resolves a font's bold/italic style. Styled fonts are declared explicitly in
/// font_style_overrides(); the name-suffix heuristic below is only a safety net and logs
/// a warning when it detects a styled font that is not declared, so adding or renaming a
/// font surfaces loudly instead of silently losing or gaining weight.
/// @param {real} _base_font The font asset intended for this text.
/// @returns {Struct}
static _font_style = function(_base_font) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
var _name = font_get_name(_base_font);
var _overrides = font_style_overrides();
if (struct_exists(_overrides, _name)) {
return _overrides[$ _name];
}

var _style = { bold: false, italic: false };
var _len = string_length(_name);
if (_len == 0) {
return _style;
}

var _is_digit = function(_char) {
return string_digits(_char) == _char;
};

var _last = string_char_at(_name, _len);
if (_is_digit(_last)) {
return _style;
}
if (_last != "b" && _last != "i") {
return _style;
}

LOGGER.warning($"Font '{_name}' suggests bold/italic via its name suffix but is not declared in font_style_overrides(); add it there so the CJK fallback weight is explicit.");

_style.bold = _last == "b";
_style.italic = _last == "i";

if (_len > 1) {
var _prev = string_char_at(_name, _len - 1);
if (_is_digit(_prev)) {
return _style;
}
if (_last == "i" && _prev == "b") {
_style.bold = true;
} else if (_last == "b" && _prev == "i") {
_style.italic = true;
}
}
return _style;
};
}

/// @desc Global shorthand for speaking localized text. Falls back to the raw English key.
Expand All @@ -121,6 +277,18 @@ function localize(_key, _args = undefined) {
return _key;
}

/// @desc Global shorthand for localizing every entry of an array in one call, mapping each
/// English element to the current language. Missing keys fall back to English and are
/// flagged with LOGGER.error.
/// @param {Array} _keys Array of English keys and/or { text, variables } structs.
/// @returns {Array}
function localize_array(_keys) {
if (variable_global_exists("localization_manager")) {
return global.localization_manager.localize_array(_keys);
}
return _keys;
}

/// @desc Global shorthand for a font suitable for the current language, deriving
/// the point size from the base font asset so callers never hardcode sizes.
/// @param {real} _base_font The font asset intended for this text.
Expand All @@ -132,3 +300,18 @@ function cjk_font(_base_font) {
}
return _base_font;
}

/// @desc Explicit registry of styled font assets used by the CJK fallback. The CJK runtime font
/// cannot preserve bold/italic, so styled fonts must be declared here; the name-suffix
/// heuristic in LocalizationManager._font_style() is only a safety net and warns loudly if a
/// styled-looking font is missing from this list. Add any future styled font here.
/// @returns {Struct}
function font_style_overrides() {
static _overrides = {
fnt_40k_14b: { bold: true, italic: false },
fnt_40k_30b: { bold: true, italic: false },
fnt_40k_12i: { bold: false, italic: true },
fnt_40k_14i: { bold: false, italic: true },
};
return _overrides;
}
1 change: 1 addition & 0 deletions scripts/SettingsManager/SettingsManager.gml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function SettingsManager() constructor {
static apply_language = function() {
global.language = language;
global.localization_manager.load_language(language);
global.localization_manager.refresh_locale_globals();
};

static sync_ui = function() {
Expand Down
5 changes: 3 additions & 2 deletions scripts/UIRenderComponents/UIRenderComponents.gml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ function UITextRendererComponent(owner, name) : UIRenderComponent(owner, name) c
return;
}
callback(self);
if (draw_get_font() != font) {
draw_set_font(font);
var _effective_font = cjk_font(font);
if (draw_get_font() != _effective_font) {
draw_set_font(_effective_font);
}
var orig_valign = draw_get_valign();
var orig_halign = draw_get_halign();
Expand Down
Loading
Loading