Skip to content

Latest commit

 

History

History
519 lines (407 loc) · 13.3 KB

README.md

File metadata and controls

519 lines (407 loc) · 13.3 KB

smiti18n

A very complete internationalization library for Lua with LÖVE support 🌕💕

Test 🧪 Lua Versions LÖVE Versions LuaRocks License

Introduction

smiti18n (pronounced smitten) is a powerful internationalization (i18n) library that helps you create multilingual applications in Lua and LÖVE.

Core Features: 🚀

  • 📁 Smart file-based loading & fallbacks
  • 🔠 Rich text interpolation & pluralization
  • 🌏 Locale-aware formatting for numbers, dates and currency
  • 💕 Built for LÖVE game engine

Rich Game Content: 🎮

  • 💬 Complex dialogue support:
    • Branching conversations
    • Character-specific translations
    • Context-aware responses
  • 🎯 53 locales, 650+ game-specific phrases
  • 📊 36 regional number formats

An intuitive API for managing translations forked from i18n.lua by Enrique García Cota incorporating a collection of community contributions. The number, date and time formatting has been ported from Babel. Includes translations from PolyglotGamedev. Significantly expanded test coverage and documentation.

Requirements

  • Lua 5.1-5.4 or LuaJIT 2.0-2.1
  • LÖVE 11.0+ (optional)

Quick example

Here's a quick example:

i18n = require 'smiti18n'

-- Load some translations
i18n.load({
  en = {
    greeting = "Hello %{name}!",
    messages = {
      one = "You have one new message",
      other = "You have %{count} new messages"
    }
  },
  es = {
    greeting = "¡Hola %{name}!",
    messages = {
      one = "Tienes un mensaje nuevo",
      other = "Tienes %{count} mensajes nuevos"
    }
  }
})

-- Set the current locale
i18n.setLocale('es')

-- Use translations
print(i18n('greeting', {name = "Luna"}))     -- ¡Hola Luna!
print(i18n('messages', {count = 3}))         -- Tienes 3 mensajes nuevos

Installation

Using LuaRocks

smiti18n is available on LuaRocks. You can install it with the following command:

luarocks install smiti18n

Manual Installation

Option 1: Git Clone

Clone this repository and copy the smiti18n folder into your project as something like lib/smiti18n.

git clone https://github.com/Oval-Tutu/smiti18n.git
cd smiti18n
cp -r smiti18n your-project/lib/

Option 2: Download Release

  1. Download latest release from Releases
  2. Extract the archive
  3. Copy smiti18n directory to your project

Project structure after installation:

your-project/
  ├── lib/
  │   └── smiti18n/
  │       ├── init.lua
  │       ├── interpolate.lua
  │       ├── plural.lua
  │       ├── variants.lua
  │       └── version.lua
  ├── locales/
  │   ├── en-UK.lua
  │   └── other-locale-files.lua
  └── main.lua

Using the Library

-- Require smiti18n in your code
local i18n = require 'lib.smiti18n'
i18n.loadFile('locales/en-UK.lua')
i18n.setLocale('en-UK')

Translation Files

smiti18n supports both single-file and multi-file approaches for managing translations.

Single File

Store all translations in one file (e.g., translations.lua):

-- translations.lua
return {
  en = {
    greeting = "Hello!",
    messages = {
      one = "You have one message",
      other = "You have %{count} messages"
    }
  },
  es = {
    greeting = "¡Hola!",
    messages = {
      one = "Tienes un mensaje",
      other = "Tienes %{count} mensajes"
    }
  }
}

-- Load translations
i18n.loadFile('translations.lua')

Multiple Files

Organize translations by language (recommended for larger projects):

Here's an example project structure:

locales/
  ├── en.lua   -- English translations
  ├── es.lua   -- Spanish translations
  └── fr.lua   -- French translations

en.lua

return {
  en = {  -- Locale key required
    greeting = "Hello!",
    messages = {
      one = "You have one message",
      other = "You have %{count} messages"
    }
  }
}
i18n.loadFile('locales/en.lua') -- English translation
i18n.loadFile('locales/es.lua') -- Spanish translation
i18n.loadFile('locales/fr.lua') -- French translation

Key Points

  • Files must include locale key in returned table
  • Can be loaded in any order
  • Later loads override earlier translations

Locales and Fallbacks

smiti18n provides flexible locale support with automatic fallbacks and regional variants.

Locale Naming

  • Pattern: language-REGION (e.g., 'en-US', 'es-MX', 'pt-BR')
  • Separator: hyphen (-) only
  • Not supported: underscores, spaces, or other separators

Fallback Chain

smiti18n implements a robust fallback system:

  1. Current Locale ('es-MX')
  2. Parent Locales (if defined, e.g., 'es-419')
  3. Root Locale ('es')
  4. Default Value (if provided)
  5. nil (if no matches found)
-- Example showing fallback chain
i18n.load({
  es = {
    greeting = "¡Hola!",
  },
  ["es-MX"] = {
    farewell = "¡Adiós!"
  }
})

i18n.setLocale('es-MX')
print(i18n('farewell'))                -- "¡Adiós!"    (from es-MX)
print(i18n('greeting'))                -- "¡Hola!"     (from es)
print(i18n('missing'))                 -- nil          (not found)
print(i18n('missing', {
    default = 'Not found'
}))                                    -- "Not found"   (default value)

Multiple Locales

For handling regional variants, you can specify multiple locales in order of preference:

i18n.load({
  ['es-419'] = { cookie = 'galleta' },    -- Latin American
  ['es-ES']  = { cookie = 'galletita' },  -- European
  ['es']     = { thanks = 'gracias' }     -- Generic
})

-- Set multiple locales in priority order
i18n.setLocale({'es-419', 'es-ES', 'es'})

i18n('cookie')  -- Returns 'galleta' (from es-419)
i18n('thanks')  -- Returns 'gracias' (from es)

Key benefits of multiple locales:

  • Handle regional variations (e.g., pt-BR vs pt-PT)
  • Share base translations across regions
  • Create fallback chains (e.g., es-MX → es-419 → es)
  • Support partial translations with automatic fallback

💡NOTE! Locales are tried in order of preference, with duplicates automatically removed.

String Interpolation

smiti18n supports three different styles of variable interpolation:

Named Variables (Recommended)

Named variables are the recommended approach as they make translations more maintainable and less error-prone.

i18n.set('greeting', 'Hello %{name}, you are %{age} years old')
i18n('greeting', {name = 'Alice', age = 25})  -- Hello Alice, you are 25 years old

Lua Format Specifiers

i18n.set('stats', 'Score: %d, Player: %s')
i18n('stats', {1000, 'Bob'})  -- Score: 1000, Player: Bob

Advanced Formatting

i18n.set('profile', 'User: %<name>.q | Age: %<age>.d | Level: %<level>.o')
i18n('profile', {
    name = 'Charlie',
    age = 30,
    level = 15
})  -- User: Charlie | Age: 30 | Level: 17k

Format modifiers:

  • .q: Quotes the value
  • .d: Decimal format
  • .o: Ordinal format

Pluralization

smiti18n implements the CLDR plural rules for accurate pluralization across different languages. Each language can have different plural categories like 'one', 'few', 'many', and 'other'.

Basic Usage

i18n = require 'smiti18n'

i18n.load({
  en = {
    msg = {
      one   = "one message",
      other = "%{count} messages"
    }
  },
  ru = {
    msg = {
      one   = "1 сообщение",
      few   = "%{count} сообщения",  -- 2-4 messages
      many  = "%{count} сообщений",  -- 5-20 messages
      other = "%{count} сообщения"   -- fallback
    }
  }
})

-- English pluralization
i18n.setLocale('en')
print(i18n('msg', {count = 1}))  -- "one message"
print(i18n('msg', {count = 5}))  -- "5 messages"

-- Russian pluralization
i18n.setLocale('ru')
print(i18n('msg', {count = 1}))  -- "1 сообщение"
print(i18n('msg', {count = 3}))  -- "3 сообщения"
print(i18n('msg', {count = 5}))  -- "5 сообщений"

💡NOTE! The count parameter is required for plural translations.

Custom Pluralization Rules

For special cases or invented languages, you can define custom pluralization rules by specifying a custom pluralization function in the second parameter of setLocale().

-- Custom pluralization for a constructed language
local customPlural = function(n)
  if n == 0 then return 'zero' end
  if n == 1 then return 'one' end
  if n > 1000 then return 'many' end
  return 'other'
end

i18n.setLocale('conlang', customPlural)

This function must return a plural category when given a number. Available plural categories:

  • zero: For languages with special handling of zero
  • one: Singular form
  • two: Special form for two items
  • few: For languages with special handling of small numbers
  • many: For languages with special handling of large numbers
  • other: Default fallback form

Arrays

Translation values can be arrays for handling ordered collections of strings with support for interpolation and pluralization.

i18n.load({
  en = {
    -- Simple array of strings
    greetings = {"Hello!", "Hi there!", "Howdy!"},

    -- Get a random greeting
    print(i18n('greetings')[math.random(#i18n('greetings'))])
  }
})

Features

Arrays support:

  • Plain strings
  • Interpolated values
  • Plural forms
  • Nested arrays
  • Mixed content types

Common Use Cases

  1. Dialogue Systems
i18n.load({
  en = {
    dialogue = {
      "Detective: What brings you here?",
      "Witness: I saw everything %{time}.",
      {
        one = "Detective: Just %{count} witness?",
        other = "Detective: Already %{count} witnesses."
      }
    }
  }
})

-- Play through dialogue sequence
for _, line in ipairs(i18n('dialogue', {time = "last night", count = 1})) do
  print(line)
end
  1. Tutorial Steps
i18n.load({
  en = {
    tutorial = {
      "Welcome to %{game_name}!",
      {
        one = "You have %{lives} life - be careful!",
        other = "You have %{lives} lives remaining."
      },
      "Use WASD to move",
      "Press SPACE to jump"
    }
  }
})
  1. Status Displays
i18n.load({
  en = {
    status = {
      "=== Game Status ===",
      "Player: %{name}",
      {
        one = "%{coins} coin collected",
        other = "%{coins} coins collected"
      },
      "Level: %{level}",
      "=================="
    }
  }
})

Tips

  • Arrays maintain their order
  • Access individual elements with numeric indices
  • Use # operator to get array length
  • Combine with math.random() for random selection
  • Arrays can be nested for complex dialogue trees

Formats

The library provides utilities for formatting numbers, prices and dates according to locale conventions. Formats are configured through locale files using the _formats key. If no locale-specific formats are defined, the library falls back to ISO standard formats.

-- Example locale file (en-UK.lua)
return {
  ["en-UK"] = {
    _formats = {
      currency = {
        symbol = "£",
        decimal_symbol = ".",
        thousand_separator = ",",
        positive_format = "%c %p%q"  -- £ 99.99
      },
      number = {
        decimal_symbol = ".",
        thousand_separator = ","
      },
      date_time = {
        long_date = "%l %d %F %Y",  -- Monday 25 March 2024
        short_time = "%H:%M"        -- 15:45
      }
    }
  }
}

Usage

i18n.loadFile('locales/en-UK.lua')
i18n.setLocale('en-UK')

-- Numbers
local num = i18n.formatNumber(1234.56)  -- "1,234.56"

-- Currency
local price = i18n.formatPrice(99.99)   -- "£ 99.99"

-- Dates
local date = i18n.formatDate("long_date")  -- "Monday 25 March 2024"

ISO Fallbacks

When no locale formats are configured, falls back to:

  • Numbers: ISO 31-0 (space separator, point decimal) - "1 234.56"
  • Currency: ISO 4217 (XXX symbol) - "XXX 99.99"
  • Dates: ISO 8601 - "2024-03-25T15:45:30"

Format Patterns

Dates use strftime codes: %Y year, %m month, %d day, %H hour, %M minute.

  • Currency patterns use:
    • %c - currency symbol
    • %q - formatted amount
    • %p - polarity sign (+/-)

For a complete reference implementation of all format options, see spec/en-UK.lua.

Contributing

Contributions are welcome! Please feel free to submit a pull request.

Specs

This project uses busted for its specs. If you want to run the specs, you will have to install it first. Then just execute the following from the root inspect folder:

busted