Add [locale] inline tag for per-locale fonts

New `[locale='xx']...[/locale]` markup lets GUI text sections render
with a locale-specific font (e.g. CJK) while the rest of the caption
keeps the current game font.

guiObject.caption = "Hello [locale='ja']世界[/locale], how do you
[locale='zh']感覺[/locale]" This is ideal for language pickers and
similar UI where you want the language name shown in its own script.

This commit unlocks richer, self-explanatory international UI while
keeping the legacy text behaviour unchanged.
This commit is contained in:
trompetin17 2025-07-02 08:43:04 -05:00
parent 47454a592e
commit 690838e3dc
No known key found for this signature in database
GPG key ID: 6C585FA3FC5DB179
15 changed files with 56 additions and 20 deletions

View file

@ -110,11 +110,22 @@ CFontManager::CFontManager()
});
}
std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName)
std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName, CStrIntern locale)
{
const std::string locale{g_L10n.GetCurrentLocale() != icu::Locale::getUS() ? g_L10n.GetCurrentLocaleString() : ""};
const std::string localeToUse{[&]
{
if (!locale.empty())
return locale.string();
if (g_L10n.GetCurrentLocale() == icu::Locale::getUS())
return std::string{};
// Use the current locale, but not US English.
return g_L10n.GetCurrentLocaleString();
} ()
};
const float guiScale{g_ConfigDB.Get("gui.scale", 1.0f)};
CStrIntern localeFontName{fmt::format("{}{}-{}", locale ,fontName.string(), guiScale)};
CStrIntern localeFontName{fmt::format("{}{}-{}", localeToUse ,fontName.string(), guiScale)};
FontsMap::iterator it{m_Fonts.find(localeFontName)};
if (it != m_Fonts.end())
@ -148,13 +159,13 @@ std::shared_ptr<CFont> CFontManager::LoadFont(CStrIntern fontName)
// TODO: explicit Locale like RTL or Arabic fonts.
// 1. Locale-specific fonts first
if (!locale.empty())
if (!localeToUse.empty())
{
if (fontSpec.bold)
candidateFonts.push_back(fmt::format("fonts.{}.{}.bold", locale, fontSpec.type));
candidateFonts.push_back(fmt::format("fonts.{}.{}.bold", localeToUse, fontSpec.type));
if (fontSpec.italic)
candidateFonts.push_back(fmt::format("fonts.{}.{}.italic", locale, fontSpec.type));
candidateFonts.push_back(fmt::format("fonts.{}.{}.regular", locale, fontSpec.type));
candidateFonts.push_back(fmt::format("fonts.{}.{}.italic", localeToUse, fontSpec.type));
candidateFonts.push_back(fmt::format("fonts.{}.{}.regular", localeToUse, fontSpec.type));
}
// 2. Then global fonts

View file

@ -40,7 +40,7 @@ public:
~CFontManager() = default;
NONCOPYABLE(CFontManager);
std::shared_ptr<CFont> LoadFont(CStrIntern fontName);
std::shared_ptr<CFont> LoadFont(CStrIntern fontName, CStrIntern locale);
void UploadTexturesAtlasToGPU();
private:

View file

@ -25,8 +25,13 @@
#include "renderer/Renderer.h"
CFontMetrics::CFontMetrics(CStrIntern font)
: CFontMetrics(font, CStrIntern())
{
m_Font = g_Renderer.GetFontManager().LoadFont(font);
}
CFontMetrics::CFontMetrics(CStrIntern font, CStrIntern locale)
{
m_Font = g_Renderer.GetFontManager().LoadFont(font, locale);
}
float CFontMetrics::GetHeight() const

View file

@ -32,6 +32,7 @@ class CFontMetrics
{
public:
CFontMetrics(CStrIntern font);
CFontMetrics(CStrIntern font, CStrIntern locale);
float GetHeight() const;
float GetCapHeight();

View file

@ -55,7 +55,7 @@ CTextRenderer::CTextRenderer()
{
ResetTranslate();
SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
SetCurrentFont(str_sans_10);
SetCurrentFont(str_sans_10, CStrIntern{});
}
void CTextRenderer::ResetTranslate(const CVector2D& translate)
@ -84,12 +84,12 @@ void CTextRenderer::SetCurrentColor(const CColor& color)
}
}
void CTextRenderer::SetCurrentFont(CStrIntern font)
void CTextRenderer::SetCurrentFont(CStrIntern font, CStrIntern locale)
{
if (font != m_FontName)
{
m_FontName = font;
m_Font = g_Renderer.GetFontManager().LoadFont(font);
m_Font = g_Renderer.GetFontManager().LoadFont(font, locale);
m_Dirty = true;
}
}

View file

@ -61,7 +61,7 @@ public:
/**
* Set the font for subsequent print calls.
*/
void SetCurrentFont(CStrIntern font);
void SetCurrentFont(CStrIntern font, CStrIntern locale);
/**
* Print formatted text at (0,0) under the current transform,

View file

@ -471,7 +471,7 @@ void CGUIText::Draw(CGUI& pGUI, CCanvas2D& canvas, const CGUIColor& DefaultColor
continue;
textRenderer.SetCurrentColor(tc.m_UseCustomColor ? tc.m_Color : DefaultColor);
textRenderer.SetCurrentFont(tc.m_Font);
textRenderer.SetCurrentFont(tc.m_Font, tc.m_FontLocale);
textRenderer.Put(pos.X + tc.m_Pos.X, pos.Y + tc.m_Pos.Y, &tc.m_String);
}

View file

@ -124,6 +124,19 @@ public:
*/
CStrIntern m_Font;
/**
* BCP-47 locale tag (e.g. `"en"`, `"ja"`, `"zh-hans"`) that tells the
* FontManager which per-locale font to use for this portion of text.
*
* If the string is non-empty, the renderer tries to substitute a font
* registered for that locale; otherwise it falls back to the game-wide
* UI font so legacy captions behave exactly as before.
*
* The value is filled in by the `[locale='xx'][/locale]` inline-markup
* parser and read during glyph layout.
*/
CStrIntern m_FontLocale;
/**
* Settings
*/

View file

@ -1255,7 +1255,7 @@ void CInput::DrawContent(CCanvas2D& canvas)
const float ls{font.GetHeight()};
CTextRenderer textRenderer;
textRenderer.SetCurrentFont(font_name);
textRenderer.SetCurrentFont(font_name, CStrIntern{});
textRenderer.Translate(
(float)(int)(m_CachedActualSize.left) + m_BufferZone,

View file

@ -207,6 +207,9 @@ void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrInt
// TODO Gee: (2004-08-15) Check if Font exists?
TextCall.m_Font = CStrIntern(utf8_from_wstring(tag.m_TagValue));
break;
case TextChunk::Tag::TAG_LOCALE:
TextCall.m_FontLocale = CStrIntern(utf8_from_wstring(tag.m_TagValue));
break;
case TextChunk::Tag::TAG_TOOLTIP:
TextCall.m_Tooltip = tag.m_TagValue;
break;
@ -219,7 +222,7 @@ void CGUIString::GenerateTextCall(const CGUI& pGUI, SFeedback& Feedback, CStrInt
// Calculate the size of the font.
CSize2D size;
float cx, cy;
CFontMetrics font (TextCall.m_Font);
CFontMetrics font{TextCall.m_Font, TextCall.m_FontLocale};
font.CalculateStringSize(TextCall.m_String.c_str(), cx, cy);
size.Width = cx;
@ -273,6 +276,8 @@ CGUIString::TextChunk::Tag::TagType CGUIString::TextChunk::Tag::GetTagType(const
return TAG_COLOR;
if (tagtype == L"font")
return TAG_FONT;
if (tagtype == L"locale")
return TAG_LOCALE;
if (tagtype == L"icon")
return TAG_ICON;
if (tagtype == L"imgleft")

View file

@ -66,6 +66,7 @@ public:
TAG_B,
TAG_I,
TAG_FONT,
TAG_LOCALE,
TAG_SIZE,
TAG_COLOR,
TAG_IMGLEFT,

View file

@ -225,7 +225,7 @@ void CConsole::Render(CCanvas2D& canvas)
DrawWindow(canvas);
CTextRenderer textRenderer;
textRenderer.SetCurrentFont(CStrIntern(m_consoleFont));
textRenderer.SetCurrentFont(CStrIntern{m_consoleFont}, CStrIntern{});
// Animation: slide in from top of screen.
const float deltaY = (1.0f - m_VisibleFrac) * m_Height;
textRenderer.Translate(m_X, m_Y - deltaY);

View file

@ -182,7 +182,7 @@ void CLogger::Render(CCanvas2D& canvas)
float height{font.GetHeight()};
CTextRenderer textRenderer;
textRenderer.SetCurrentFont(font_name);
textRenderer.SetCurrentFont(font_name, CStrIntern{});
textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
// Offset by an extra 35px vertically to avoid the top bar.

View file

@ -200,7 +200,7 @@ void CProfileViewer::RenderProfile(CCanvas2D& canvas)
// Print table and column titles.
CTextRenderer textRenderer;
textRenderer.SetCurrentFont(font_name);
textRenderer.SetCurrentFont(font_name, CStrIntern{});
textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
textRenderer.PrintfAt(2.0f, height, L"%hs", table->GetTitle().c_str());
textRenderer.Translate(22.0f, height*2.0f);

View file

@ -793,7 +793,7 @@ void TerrainRenderer::RenderPriorities(CCanvas2D& canvas, int cullGroup)
ENSURE(m->phase == Phase_Render);
CTextRenderer textRenderer;
textRenderer.SetCurrentFont(CStrIntern("mono-stroke-10"));
textRenderer.SetCurrentFont(CStrIntern{"mono-stroke-10"}, CStrIntern{});
textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 1.0f));
std::vector<CPatchRData*>& visiblePatches = m->visiblePatches[cullGroup];