A very complete internationalization library for Lua with LÖVE support 🌕💕
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.
- Lua 5.1-5.4 or LuaJIT 2.0-2.1
- LÖVE 11.0+ (optional)
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
smiti18n is available on LuaRocks. You can install it with the following command:
luarocks install smiti18n
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/
- Download latest release from Releases
- Extract the archive
- 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
-- Require smiti18n in your code
local i18n = require 'lib.smiti18n'
i18n.loadFile('locales/en-UK.lua')
i18n.setLocale('en-UK')
smiti18n supports both single-file and multi-file approaches for managing translations.
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')
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
…
- Files must include locale key in returned table
- Can be loaded in any order
- Later loads override earlier translations
smiti18n provides flexible locale support with automatic fallbacks and regional variants.
- Pattern:
language-REGION
(e.g., 'en-US', 'es-MX', 'pt-BR') - Separator: hyphen (-) only
- Not supported: underscores, spaces, or other separators
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)
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.
smiti18n supports three different styles of variable interpolation:
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
i18n.set('stats', 'Score: %d, Player: %s')
i18n('stats', {1000, 'Bob'}) -- Score: 1000, Player: Bob
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
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'.
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.
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
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'))])
}
})
Arrays support:
- Plain strings
- Interpolated values
- Plural forms
- Nested arrays
- Mixed content types
- 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}",
"=================="
}
}
})
- 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
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
}
}
}
}
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"
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"
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.
Contributions are welcome! Please feel free to submit a pull request.
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