1
Languages
minjaesong edited this page 2025-11-24 21:24:45 +09:00

Languages

Terrarum features comprehensive internationalisation (i18n) support with over 20 languages, a custom multilingual font system, and sophisticated text rendering including support for complex scripts.

Overview

The language system provides:

  • 20+ supported languages — From English to Japanese to Hindi
  • Terrarum Sans Bitmap — Custom multilingual bitmap font
  • String templates — Dynamic text formatting
  • Pluralisation — Language-specific plural rules
  • Postpositions — Korean/Japanese grammatical particles
  • Fallback system — Graceful handling of missing translations
  • Polyglot support — Integration with Polyglot format

Language Codes

Languages are identified by ISO locale codes:

Supported Languages

Code Language
en English
koKR Korean
jaJP Japanese
zhCN Chinese (Simplified)
de German
frFR French
es Spanish
it Italian
ptBR Portuguese (Brazilian)
ruRU Russian
plPL Polish
nlNL Dutch
daDK Danish
noNB Norwegian (Bokmål)
fiFI Finnish
isIC Icelandic
csCZ Czech
huHU Hungarian
bgBG Bulgarian
elGR Greek
hiIN Hindi

Lang Object

The Lang singleton manages all translations:

Getting Translations

// Basic usage
val text = Lang["STRING_ID"]

// With capitalisation
val capitalised = Lang["string_id", capitalise = true]

// Safe access (returns null if missing)
val text = Lang.getOrNull("STRING_ID")

String ID Format

Translation keys follow a naming convention:

CONTEXT_SPECIFIC_DESCRIPTION

Examples:
MENU_LANGUAGE_THIS       // Menu item
BLOCK_STONE_NAME         // Block name
ITEM_PICKAXE_DESC        // Item description
UI_INVENTORY_TITLE       // UI text

Translation Files

File Structure

Translations are organised by language:

assets/locales/
├── en/
│   ├── main.json
│   ├── blocks.json
│   └── items.json
├── koKR/
│   ├── main.json
│   └── ...
└── jaJP/
    └── ...

Regular Format

Standard translation files use simple JSON:

{
  "MENU_NEW_GAME": "New Game",
  "MENU_LOAD_GAME": "Load Game",
  "MENU_OPTIONS": "Options",
  "BLOCK_STONE_NAME": "Stone",
  "ITEM_PICKAXE_NAME": "Pickaxe"
}

Polyglot Format

For Polyglot-compatible files (prefix: Polyglot-100_*.json):

{
  "resources": {
    "polyglot": { /* metadata */ },
    "data": [
      {
        "n": "STRING_ID",
        "s": "Translated Text"
      },
      {
        "n": "ANOTHER_STRING_ID",
        "s": "Another Translation"
      }
    ]
  }
}

String Templates

Templates allow dynamic text insertion:

Basic Template Syntax

// Translation file:
"WELCOME_MESSAGE": "Welcome, %s!"

// Usage:
val text = String.format(Lang["WELCOME_MESSAGE"], playerName)
// Result: "Welcome, Steve!"

Bind Operator

The >>= operator binds strings to templates:

// Define template
"WALL_NAME_TEMPLATE": "%s Wall"

// Use bind operator in item names
item.originalName = "BLOCK_STONE>>=WALL_NAME_TEMPLATE"

// Result: "Stone Wall"

This is particularly useful for generating variants:

"BLOCK_WOOD_OAK"   // "Oak Wood"
"BLOCK_WOOD_OAK>>=WALL_NAME_TEMPLATE"  // "Oak Wood Wall"

Advanced Features

Korean Postpositions

Korean requires dynamic postposition selection based on the final character:

// Automatic postposition selection
val name = "사과"  // Apple (ends with vowel)
Lang.formatKoreanPostposition(name, "은는")  // Returns "는"

val name2 = "돌"  // Stone (ends with consonant)
Lang.formatKoreanPostposition(name2, "은는")  // Returns "은"

The system supports:

  • 은/는 — Topic marker
  • 이/가 — Subject marker
  • 을/를 — Object marker
  • 로/으로 — Direction marker

Pluralisation

Different languages have different plural rules:

English

// Regular plurals: add 's'
"apple" -> "apples"

// Irregular handled specially
"photo", "demo" -> normal 's' plural

French

// Special words with normal 's' plural
"bal", "banal", "fatal", "final"

// Others follow French rules

The pluralisation system is extensible for each language.

Capitalisation

Automatic capitalisation respects language rules:

val text = Lang["menu_item", capitalise = true]
// English: "Menu Item"
// German: "Menü-Element" (noun capitalisation rules)

Terrarum Sans Bitmap

The game uses a custom bitmap font supporting all languages:

Font Features

  • Multilingual — Latin, Cyrillic, Greek, CJK, Hangul, Devanagari
  • Bitmap-based — Pixel-perfect rendering at any size
  • Fallback chains — Graceful degradation for missing glyphs
  • SDF rendering — Smooth scaling with signed distance fields

Font Sizes

The font system provides multiple sizes:

  • 24pt — UI text
  • 32pt — Large text, headings
  • 48pt — Very large text
  • BigAlphNum — Numbers and basic letters
  • TinyAlphNum — Tiny fallback font

See: Terrarum Sans Bitmap Repository

Adding Translations

For Modules

Add language files in your module:

<module>/locales/
├── en/
│   └── mymod.json
└── koKR/
    └── mymod.json

Register them with the language loader:

ModMgr.GameLanguageLoader.loadAll(
    moduleInfo,
    File("<module>/locales/")
)

Translation File Example

{
  "MYMOD_ITEM_SPECIAL_SWORD": "Special Sword",
  "MYMOD_ITEM_SPECIAL_SWORD_DESC": "A sword with special properties",
  "MYMOD_BLOCK_MAGIC_STONE": "Magic Stone",
  "MYMOD_UI_CRAFTING_TITLE": "Magical Crafting"
}

Best Practises

  1. Use prefixes — Namespace your string IDs with module name
  2. Group by context — Separate files for items, blocks, UI
  3. Provide fallbacks — Always have English (en) translations
  4. Keep keys stable — Don't change IDs once released
  5. Use templates — Avoid duplicating similar strings
  6. Test all languages — Ensure text fits in UI elements

Missing Translations

The system handles missing translations gracefully:

val text = Lang["NONEXISTENT_KEY"]
// Returns: "$NONEXISTENT_KEY"  (with $ prefix)

The $ prefix indicates a missing translation. Check logs for warnings.

Fallback Chain

  1. Try requested language (e.g., koKR)
  2. Fall back to English (en)
  3. Return $KEY if not found

Language Selection

Setting Language

App.GAME_LOCALE = "koKR"  // Set to Korean
Lang.load(File("./assets/locales/"))  // Reload translations

Current Language

val currentLang = App.GAME_LOCALE

Available Languages

val languages: Set<String> = Lang.languageList

Caching System

The Lang object caches decoded strings for performance:

// Cached per locale
private val decodeCache = HashMap<String, HashMap<String, String>>()

Cache is cleared when language changes.

Name Generation

The system includes name generators for various cultures:

Namesets

Namesets are CSV files defining name components:

assets/locales/nameset_scandinavian_m.csv
assets/locales/nameset_scandinavian_f.csv
assets/locales/nameset_russian_m.csv
assets/locales/nameset_russian_f.csv
assets/locales/nameset_dwarven.csv
assets/locales/nameset_exotic_deities.csv

Using Namesets

val name = RandomWordsName.generate("scandinavian_m")
// Returns: "Ragnar", "Bjorn", "Olaf", etc.

Language Properties

Language-specific properties are defined in langprop.csv:

lang,direction,font,features
en,ltr,latin,plural_s
koKR,ltr,hangul,postposition
jaJP,ltr,japanese,
ar,rtl,arabic,rtl_text

Properties include:

  • direction — Text direction (ltr/rtl)
  • font — Primary font family
  • features — Language-specific features

Advanced Usage

Dynamic Text Generation

Generate text with multiple variables:

val template = Lang["CRAFTING_RECIPE_YIELDS"]
// "Crafting %s with %s yields %d %s"

val text = String.format(
    template,
    item1Name,
    item2Name,
    quantity,
    resultName
)

Formatting Numbers

Use locale-aware number formatting:

val formatter = NumberFormat.getInstance(Locale(App.GAME_LOCALE))
val formatted = formatter.format(12345.67)
// English: "12,345.67"
// German:  "12.345,67"
// French:  "12 345,67"

Handling Long Text

For UI elements, check text width:

val font = App.fontGame
val width = font.getWidth(translatedText)

if (width > maxWidth) {
    // Truncate or wrap text
}

Keyboard Input

See also: Keyboard Layout and IME

The IME system allows input in any language:

  • Low Layer — Base keyboard (e.g., QWERTY)
  • High Layer — Language-specific input (e.g., Korean Hangul)

Common Patterns

Item Names with Templates

class MyBlock : BlockBase() {
    init {
        originalName = "BLOCK_MYBLOCK>>=BLOCK_WALL_NAME_TEMPLATE"
        // Automatically generates "My Block Wall"
    }
}

UI Text with Variables

fun showInventoryCount(count: Int) {
    val text = String.format(
        Lang["UI_INVENTORY_ITEMS_COUNT"],
        count
    )
    // "Items: 42"
}

Safe Translation Access

val text = Lang.getOrNull("OPTIONAL_STRING_ID") ?: "Default Text"

Debugging

Finding Missing Translations

Search for $ in rendered text:

if (text.startsWith("$")) {
    println("Missing translation: ${text.substring(1)}")
}

Logging Untranslated Strings

Enable debug logging:

App.IS_DEVELOPMENT_BUILD = true

Missing translations will be logged to console.

Performance Considerations

  1. Cache translations — Don't call Lang[] every frame
  2. Preload common strings — Load frequently-used text at init
  3. Avoid string concatenation — Use templates instead
  4. Batch font measurements — Measure multiple strings together

Limitations

  • No RTL support yet — Right-to-left languages not fully implemented
  • Limited plural forms — Only basic plural rules
  • Static at runtime — Language changes require restart
  • No gender agreement — Some languages need grammatical gender

See Also