MithrilJS simple localization

Introduction

When I first started learning Mithril.js, I focused on getting things to just work. But as the app grew, I realized it needed localization—the ability to display text in different languages.

If you’ve ever worked on a multi-language app, you know it can be messy: scattered strings, hard-coded labels, and a growing list of “Where is that text coming from?” moments.

So I built a simple, reusable localization module for my Mithril.js project. In this post, I’ll walk you through how it works, how I store translations, and how I use them in my views.

The idea

The goal was simple:

  • Keep translations in JSON files.
  • Store the current language in localStorage so it persists.
  • Load translations only when needed.
  • Use a short i18n.t('some.key') syntax to fetch text.

The Localization Module

Here’s the core of the implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import m from 'mithril';
import config from '../../config/config';
import StorageHelper from './StorageHelper';


const urlTemplate = "resources/lang/message.{locale}.json";

let i18n = {
    defaultLocale: config.DEFAULT_LOCALE,
    messages: {},
    currentLocale: '',
    storageKey: 'language',

    init: function() {
        if (StorageHelper.get(i18n.storageKey)) {
            return i18n.setLocale(StorageHelper.get(i18n.storageKey));
        }
        else {
            return i18n.setLocale(i18n.getDefaultLocale());
        }
    },

    getDefaultLocale: function() {
        return i18n.defaultLocale;
    },

    t: function(key) {
        return key.split('.').reduce((a, b) => a[b], i18n.messages) || key;
    },

    setLocale: function(newLocale) {
        if (i18n.currentLocale === newLocale) {
            return Promise.resolve();
        }
        i18n.currentLocale = newLocale;
        i18n.messageUrl = urlTemplate.replace("{locale}", i18n.currentLocale);


        StorageHelper.set(i18n.storageKey, newLocale);

        if (process.env.NODE_ENV === 'development') {
            i18n.messageUrl += `?v=${Date.now()}`;
        }

        return m.request({
            method: 'GET',
            url: i18n.messageUrl
        })
        .then(function(result) {
            i18n.messages = result;
            console.log('language is loaded');
        });
    }
}

export default i18n;

How It Works

defaultLocale is the fallback language (e.g., “ja”).

messages is where all loaded translations are stored.

init()

  • Runs on app start, checks if a language is saved in localStorage.
  • If yes → loads that language.
  • If no → loads the default locale.

t(key): Gets a translation by its dot-separated path.

1
i18n.t('project.name');

setLocale(newLocale)

  • Prevents reloading if the language is already active.
  • Replaces {locale} in the URL template with the selected language.
  • Saves the chosen locale to localStorage.
  • In development, adds a timestamp query (?v=...) to avoid browser caching.
  • Fetches the JSON file and stores it in messages.

The Language Files

Here’s an example of my message.ja.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "status_value": {
        "1": "新規",
        "2": "進行中",
        "3": "完了",
        "4": "停止",
        "5": "却下"
    },
    "project": {
        "code": "コード",
        "name": "名前",
        "open": "未完了",
        "closed": "完了",
        "total": "全て"
    }
}

I keep one file per language, for example:

  • message.ja.json
  • message.en.json

Using It in a View

Here’s how you use it in a Mithril view:

1
m("div", i18n.t("project.name"))

Conclusion

This approach keeps localization simple and maintainable by separating translation data from the application logic and loading only what’s needed. While it works well for my current needs, it’s far from perfect — for example, adding browser default language detection could make it even better.