The simplest, fastest, and most powerful internationalization (i18n) plugin for Wheels 3.x+
• Lightweight • Zero dependencies • JSON or Database backed • Built-in pluralization • Full fallback support
t()– Simple key-based translation with variable interpolationtp()– Pluralization support (.zero,.one,.other)- JSON file or database translation sources
- Session-based locale switching
- Fallback locale & missing key handling
- Optional in-memory caching (great for production)
- Works anywhere in views, controllers, or models
wheels plugin install wheels-i18nAdd these settings in your config/settings.cfm file:
set(i18n_defaultLocale="en");
set(i18n_availableLocales="en,es");
set(i18n_fallbackLocale="en");
set(i18n_translationSource="json");
set(i18n_translationsPath="/app/locales");
set(i18n_cacheTranslations=false);Below is a description of all available i18n configuration settings and their default values:
| Setting Name | Default | Description |
|---|---|---|
| i18n_defaultLocale | en |
Default locale if none is set in session |
| i18n_availableLocales | en |
A comma-separated list of all supported locales: "en,es" |
| i18n_fallbackLocale | en |
Used if a translation key is missing in current locale |
| i18n_translationSource | json |
Translation method: "json" or "database" |
| i18n_translationsPath | /app/locales |
Path for JSON files (only used with json source) |
| i18n_cacheTranslations | false |
Cache translations in memory (recommended for production) |
Pro Tip: Set i18n_cacheTranslations=true in production for fast performance.
Create this file: /app/locales/en/common.json
{
"welcome": "Welcome to my app!",
"greeting": "Hello, {name}!",
"save": "Save",
"posts": {
"zero": "No Post Found",
"one": "{count} Post Found",
"other": "{count} Posts Found"
},
"nav": {
"home": "Home",
"about": {
"service": "Service",
"portfolio": "Portfolio"
},
"contact": "Contact"
}
}Same for different language: /app/locales/es/common.json
{
"welcome": "Bienvenido a nuestra aplicación",
"greeting": "¡Hola, {name}!",
"save": "Guardar",
"posts": {
"zero": "No se encontraron publicaciones",
"one": "{count} publicación encontrada",
"other": "{count} publicaciones encontradas"
},
"nav": {
"home": "Hogar",
"about": {
"service": "Servicio",
"portfolio": "Cartera"
},
"contact": "Contacto"
}
}Your application should follow the following localization structure:
/app
/locales
/en
common.json
forms.json
/es
common.json
forms.json#t("common.welcome")#
#t("common.greeting", name="Sarah")#
#t("common.nav.about.service")#
#tp("common.posts", count=5)#Your Are Done!
https://raw.githubusercontent.com/wheels-dev/wheels-i18n/main/assets/translation-via-json.mp4
Add these settings in your config/settings.cfm file:
set(i18n_defaultLocale="en");
set(i18n_availableLocales="en,es");
set(i18n_fallbackLocale="en");
set(i18n_translationSource="database");
set(i18n_cacheTranslations=false);Optional: Customize database table and column names if your schema differs.
set(i18n_dbTable="i18n_translations");
set(i18n_dbLocaleColumn="locale");
set(i18n_dbKeyColumn="translation_key");
set(i18n_dbValueColumn="translation_value");Setting Default Description
i18n_dbTable i18n_translations Translation table
i18n_dbLocaleColumn locale Locale column
i18n_dbKeyColumn translation_key Translation key
i18n_dbValueColumn translation_value Translation value
Note: These settings are only used when i18n_translationSource="database". If not defined, the plugin automatically uses the default values.
Create the database table using a standard Wheels migration:
wheels dbmigrate create table i18n_translationsThen replace the generated file with this content:
// app/migrator/migrations/XXXX_cli_create_table_i18n_translations.cfc
component {
function up() {
t = createTable(name = 'i18n_translations', force='false', id='true', primaryKey='id');
t.string(columnNames = 'locale', limit = '10', allowNull = false);
t.string(columnNames = 'translation_key', limit = '255', allowNull = false);
t.text(columnNames = 'translation_value', allowNull = false);
t.timestamps();
t.create();
addIndex(table="i18n_translations", columnNames="locale");
addIndex(table="i18n_translations", columnNames="translation_key");
}
function down() {
dropTable("i18n_translations");
}
}Then this command in CLI to run your migration:
wheels dbmigrate upIf your project already has a translations table, simply map it:
set(i18n_translationSource="database");
set(i18n_dbTable="translations");
set(i18n_dbLocaleColumn="lang");
set(i18n_dbKeyColumn="key_name");
set(i18n_dbValueColumn="value_text");
No plugin code changes are required.
Insert your translations keys according to your database to run your translation. here's a sample in MySQL
INSERT INTO i18n_translations (locale, translation_key, translation_value, createdAt, updatedAt) VALUES
('en', 'common.welcome', 'Welcome to our application', NOW(), NOW()),
('en', 'common.greeting', 'Hello, {name}!', NOW(), NOW()),
('en', 'common.goodbye', 'Goodbye', NOW(), NOW()),
('en', 'common.posts.zero', 'No Post Found', NOW(), NOW()),
('en', 'common.posts.one', '{count} Post Found', NOW(), NOW()),
('en', 'common.posts.other', '{count} Posts Found', NOW(), NOW()),
('es', 'common.welcome', 'Bienvenido a nuestra aplicación', NOW(), NOW()),
('es', 'common.greeting', '¡Hola, {name}!', NOW(), NOW()),
('es', 'common.goodbye', 'Adiós', NOW(), NOW()),
('es', 'common.posts.zero', 'Ningún Post Encontrado', NOW(), NOW()),
('es', 'common.posts.one', '{count} Post Encontrado', NOW(), NOW()),
('es', 'common.posts.other', '{count} Posts Encontrados', NOW(), NOW());
#t("common.welcome")#
#t("common.greeting", name="Sarah")#
#tp("common.posts", count=5)#Your Are Done!
https://raw.githubusercontent.com/wheels-dev/wheels-i18n/main/assets/translation-via-database.mp4
Want translators or clients to edit translations live in the browser?
You can easily build your own admin area using standard Wheels tools:
- Create a simple model mapped to the i18n_translations table
- Add a controller with index and save actions
- Build a clean view with a form (locale + key + value)
That’s it — your translators can now update text instantly.
Many agencies love this workflow. You’re in full control — build it exactly how you want.
Design Philosophy: All configuration options have sensible defaults. You only need to configure what differs from your application. This keeps setup fast while remaining fully flexible.
#t("key")#→ Translate#t("key", name="[param]")#→ With variables#tp("key", count=[param])#→ Pluralization (.zero, .one, .other)#currentLocale()#→ Get current language#changeLocale("es")#→ Switch language#availableLocales()#→ Array of supported languages
The core function to translate a key to the current locale, with parameter interpolation and fallback logic.
// Basic Usage
#t("common.welcome")# // (Output: Welcome to our application)
// With parameter interpolation
#t(key="common.greeting", name="John Doe")# // (Output: "Hello, John Dow!")Translates a key and automatically selects the correct singular (.one) or plural (.other) form based on the count argument. The count is also available for interpolation as {count}.
Note: This implementation assumes the simple English plural rule (1 is singular, anything else is plural).
// Zero usage (Count = 0)
#tp(key="common.posts", count=0)# // (Output: "No Post Found")
// Singular usage (Count = 1)
#tp(key="common.posts", count=1)# // (Output: "1 Post Found")
// Plural usage (Count > 1)
#tp(key="common.posts", count=5)# // (Output: "5 Posts Found")Sets the application locale in Session and returns a boolean based on success.
// Change to Spanish
changeLocale("es");
// Unsupported locale
changeLocale("jp"); // falseGets the current application locale from the Session, or the default locale if not set.
locale = currentLocale(); // "en"Returns an array of all configured available locales.
locales = availableLocales(); // ["en", "es", "fr"]