smiti18n
January 19, 2025 · View on GitHub
A very complete internationalization library for Lua with LÖVE support 🌕💕
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
- Download latest release from Releases
- Extract the archive
- Copy
smiti18ndirectory 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:
- Current Locale ('es-MX')
- Parent Locales (if defined, e.g., 'es-419')
- Root Locale ('es')
- Default Value (if provided)
- 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 zeroone: Singular formtwo: Special form for two itemsfew: For languages with special handling of small numbersmany: For languages with special handling of large numbersother: 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
- 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
- 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"
}
}
})
- 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