Render Slovak carons in menus and HUD without disturbing the other text

The stock menu font (Gill Sans MT Menu) is a static 243-glyph atlas with no
Latin Extended-A, so "Pokračovať" rendered as "Pokra ova" and "Podrž" as
"Podr". Dialogue, the Nomai translator and the ship log were always fine —
they use Futura PT (663) and Space Mono (612), both of which cover Slovak.

Registering a language font via AddLanguageFont fixes the menus but flips
TextTranslation.IsLanguageLatin() to false, and the game uses that flag to
decide whether dialogue, the translator and the ship log keep their own fonts
or fall back to the language font. So the blanket fix dragged all three onto
the wrong font, oversized and clipping.

Instead: register no language font at all, and postfix only the font getters
(UIStyleManager, PromptManager, GameOverController). Each result goes through
SlovakFont.Fix, which probes the font with Font.HasCharacter and returns it
untouched unless it genuinely cannot draw Slovak — so nothing that already
renders correctly is ever replaced, and the patches are inert in every other
language.

The substitute is Cabin (SIL OFL 1.1), a humanist sans in the Gill Sans
lineage, shipped as a dynamic font with its vertical metrics rewritten to Gill
Sans MT's exact ratios so line height matches and text does not clip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-21 09:02:56 +02:00
parent 41ee614d33
commit 832a92783b
6 changed files with 312 additions and 5 deletions
+6
View File
@@ -55,6 +55,12 @@ See [`CLAUDE.md`](CLAUDE.md) for the full conventions and repo layout.
The **original mod code and the Slovak translation** in this repository are
released under the [MIT License](LICENSE).
The bundled UI font in `assets/slovakfont` is **[Cabin](https://github.com/impallari/Cabin)**
by the Cabin Project Authors, used under the **SIL Open Font License 1.1**
(see [`assets/Cabin-OFL.txt`](assets/Cabin-OFL.txt)). It is only used for the few
menu/HUD elements whose stock font was never baked with Slovak caron glyphs — see
[`SlovakFont.cs`](SlovakFont.cs).
This is an **unofficial** fan project. It is **not affiliated with, approved by, or
endorsed by Mobius Digital or Annapurna Interactive.** *Outer Wilds*, its story,
dialogue, characters, and other assets are **© Mobius Digital / Annapurna
+186
View File
@@ -0,0 +1,186 @@
using HarmonyLib;
using OWML.Common;
using OWML.ModHelper;
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
namespace SlovakTranslation
{
/// <summary>
/// Fixes the missing Slovak caron glyphs (č ď ĺ ľ ň ŕ ť ž) in the menus and HUD.
///
/// Outer Wilds renders Latin languages with a handful of *static* font atlases,
/// and only some of them were baked with Latin Extended-A:
///
/// Gill Sans MT / Gill Sans MT Menu (menus, prompts) 243 glyphs — no carons
/// ParaType - Futura PT Medium (language font) 663 glyphs — complete
/// SpaceMono-Regular (translator) 612 glyphs — complete
///
/// That is why dialogue and the Nomai translator always looked right while
/// "Pokračovať" came out as "Pokra ova". Registering a whole custom language
/// font via the Localization Utility fixes the menus but also flips
/// <c>TextTranslation.IsLanguageLatin()</c> to false, which drags dialogue,
/// the translator and the ship log off their proper fonts too.
///
/// So instead we leave the language font alone and swap fonts *only* where the
/// vanilla one physically cannot draw Slovak — checked at runtime with
/// <c>Font.HasCharacter</c>, so we never touch a font that renders fine.
/// </summary>
internal static class SlovakFont
{
/// <summary>Cabin (SIL OFL 1.1), a humanist sans close to the vanilla Gill Sans MT.
/// Shipped as a dynamic font, so it can draw any glyph Slovak needs.</summary>
private static Font _font;
/// <summary>Characters the vanilla menu atlas is missing. If a font can draw all
/// of these it is left completely alone.</summary>
private static readonly char[] Probe = { 'č', 'ď', 'ĺ', 'ľ', 'ň', 'ŕ', 'ť', 'ž', 'ô', 'ä' };
private static readonly Dictionary<int, bool> _coverage = new Dictionary<int, bool>();
internal static bool Loaded => _font != null;
internal static void Load(ModBehaviour mod)
{
var path = mod.ModHelper.Manifest.ModFolderPath + "assets/slovakfont";
var bundle = AssetBundle.LoadFromFile(path);
if (bundle == null)
{
mod.ModHelper.Console.WriteLine("Could not load font bundle at " + path, MessageType.Error);
return;
}
foreach (var font in bundle.LoadAllAssets<Font>())
{
if (font.name != "SlovakUI") continue;
_font = font;
UnityEngine.Object.DontDestroyOnLoad(font);
break;
}
if (_font == null)
mod.ModHelper.Console.WriteLine("Font bundle has no 'SlovakUI' font.", MessageType.Error);
}
/// <summary>Returns <paramref name="font"/> unchanged unless Slovenčina is the
/// active language and that font cannot draw Slovak.</summary>
internal static Font Fix(Font font)
{
if (_font == null || font == null || font == _font) return font;
// Dynamic fonts rasterise straight from an embedded font file; every one the
// game ships for Latin covers Slovak, and HasCharacter is unreliable for them.
if (font.dynamic) return font;
if (!IsActive()) return font;
var id = font.GetInstanceID();
if (!_coverage.TryGetValue(id, out var complete))
{
complete = true;
foreach (var c in Probe)
{
if (font.HasCharacter(c)) continue;
complete = false;
break;
}
_coverage[id] = complete;
}
return complete ? font : _font;
}
#region "is Slovenčina selected?"
// The Localization Utility invents a TextTranslation.Language value for each
// custom language, so it is not in the enum at compile time. Ask the utility
// once, then cache the value and compare enums from then on.
private static bool _resolved;
private static TextTranslation.Language _slovak;
private static MethodInfo _tryGetLanguage;
private static object _utility;
private static bool IsActive()
{
if (TextTranslation.s_theTable == null) return false;
var current = TextTranslation.Get().GetLanguage();
if (_resolved) return current == _slovak;
if (_utility == null)
{
var type = SlovakTranslation.LocalizationApi?.GetType().Assembly.GetType("LocalizationUtility.LocalizationUtility");
if (type == null) return false;
_utility = type.GetField("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
foreach (var m in type.GetMethods())
{
var p = m.GetParameters();
if (m.Name == "TryGetLanguage" && p.Length == 2 &&
p[0].ParameterType == typeof(TextTranslation.Language))
{
_tryGetLanguage = m;
break;
}
}
if (_utility == null || _tryGetLanguage == null) return false;
}
var args = new object[] { current, null };
if (!(bool)_tryGetLanguage.Invoke(_utility, args) || args[1] == null) return false;
var name = args[1].GetType().GetProperty("Name")?.GetValue(args[1], null) as string;
if (name != SlovakTranslation.LanguageName) return false;
_slovak = current;
_resolved = true;
return true;
}
#endregion
}
/// <summary>
/// Every place the game hands out a font that is not the language font. Each one
/// runs through <see cref="SlovakFont.Fix"/>, which is a no-op unless the font
/// really is missing Slovak glyphs.
/// </summary>
[HarmonyPatch]
internal static class SlovakFontPatches
{
[HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetMenuFont))]
public static void MenuFont(ref Font __result) => __result = SlovakFont.Fix(__result);
[HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetTranslatorFont))]
public static void TranslatorFont(ref Font __result) => __result = SlovakFont.Fix(__result);
[HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetShipLogFont))]
public static void ShipLogFont(ref Font __result) => __result = SlovakFont.Fix(__result);
[HarmonyPostfix, HarmonyPatch(typeof(UIStyleManager), nameof(UIStyleManager.GetShipLogCardFont))]
public static void ShipLogCardFont(ref Font __result) => __result = SlovakFont.Fix(__result);
// Prompts ("Podrž", "Pokračovať"…) cache their font in a private field and then
// rebuild, so patch after the fact and rebuild once more.
[HarmonyPostfix, HarmonyPatch(typeof(PromptManager), "OnLanguageChanged")]
public static void PromptFont(PromptManager __instance)
{
var field = AccessTools.Field(typeof(PromptManager), "_currentFont");
var current = field.GetValue(__instance) as Font;
var fixedFont = SlovakFont.Fix(current);
if (fixedFont == current) return;
field.SetValue(__instance, fixedFont);
AccessTools.Method(typeof(PromptManager), "RebuildUI")?.Invoke(__instance, null);
}
[HarmonyPostfix, HarmonyPatch(typeof(GameOverController), nameof(GameOverController.SetupGameOverScreen))]
public static void DeathFont(GameOverController __instance)
{
var text = AccessTools.Field(typeof(GameOverController), "_deathText").GetValue(__instance) as Text;
if (text != null) text.font = SlovakFont.Fix(text.font);
}
}
}
+24 -5
View File
@@ -1,3 +1,4 @@
using HarmonyLib;
using OWML.Common;
using OWML.ModHelper;
@@ -6,21 +7,29 @@ namespace SlovakTranslation
/// <summary>
/// Registers Slovenčina as a selectable language via xen-42's Localization
/// Utility (xen.LocalizationUtility). All the heavy lifting lives in the
/// utility; this mod just points it at our translated strings.
/// utility; this mod just points it at our translated strings and patches the
/// few menu/HUD fonts that were never baked with Slovak glyphs (see
/// <see cref="SlovakFont"/>).
/// </summary>
public class SlovakTranslation : ModBehaviour
{
public static SlovakTranslation Instance;
/// <summary>Name the language is registered (and displayed) under.</summary>
public const string LanguageName = "Slovenčina";
/// <summary>Path (relative to the mod folder) to the translated strings.</summary>
public static string translationFile = "assets/Translation.xml";
/// <summary>The Localization Utility's API, kept for reflection in <see cref="SlovakFont"/>.</summary>
internal static ILocalizationAPI LocalizationApi;
private void Start()
{
Instance = this;
var api = ModHelper.Interaction.TryGetModApi<ILocalizationAPI>("xen.LocalizationUtility");
if (api == null)
LocalizationApi = ModHelper.Interaction.TryGetModApi<ILocalizationAPI>("xen.LocalizationUtility");
if (LocalizationApi == null)
{
ModHelper.Console.WriteLine(
"Could not find xen.LocalizationUtility — is Interplanetary Polyglot installed?",
@@ -28,8 +37,18 @@ namespace SlovakTranslation
return;
}
api.RegisterLanguage(this, "Slovenčina", translationFile);
ModHelper.Console.WriteLine("Registered language: Slovenčina", MessageType.Success);
LocalizationApi.RegisterLanguage(this, LanguageName, translationFile);
ModHelper.Console.WriteLine("Registered language: " + LanguageName, MessageType.Success);
// Deliberately *not* AddLanguageFont: that would make IsLanguageLatin false
// and pull dialogue, the translator and the ship log off their correct
// vanilla fonts. We only patch the fonts that can't draw Slovak.
SlovakFont.Load(this);
if (SlovakFont.Loaded)
{
new Harmony("Homer.SlovakTranslation").PatchAll();
ModHelper.Console.WriteLine("Slovak menu/HUD font (Cabin) loaded and patched in.", MessageType.Success);
}
}
}
}
+3
View File
@@ -35,5 +35,8 @@
<None Update="assets/Translation.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="assets/slovakfont">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+93
View File
@@ -0,0 +1,93 @@
Copyright 2018 The Cabin Project Authors (https://github.com/impallari/Cabin.git)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
BIN
View File
Binary file not shown.