This week I created my first official forgebox package: cbi18n-json-resources , a cbi18n JSON ResourceService. This module tries to improve the cbi18n Ortus module by offering

  • json resource files instead of java resources
  • locales organized by directory instead of partial filenames
  • optional default resource file(s)
  • hierarchical resources, so both en_GB and en_US can be handled by the same resource file, except for the different country-specific translations
  • and an interceptor for missing translations

So why this module instead of improving the cbi18n module itself?

I ‘ve been a long time cbi18n user, especially for the resource part, and for a long time it was good enough. But since I wanted to write a few more modules which need i18n functionality I needed the default resource files and hierarchical resources. So in april 2020 I rewrote the cbi18n module and added this functionality and rewrote all models in this module from tag-based to script, since it was a very old module. I also wrote brand new documentation, because the old docs are a bit outdated. Now it is januari 2021 and this module is still not published, including my not so brand new docs.

I don’t want to blame anyone, but making major contributions to coldbox modules can be a tricky process. You have to know about the build proces, start pestering people about failing builds, conform to formatting rules and coding conventions, asking more questions and there are not too many examples. The whole learning and review curve can be a bit slow and sometimes frustrating. The problem with the cbi18n module is that cbvalidation and cborm depend on it, so it is not so easy to just bump the version on ONE module. So although the new cbi18n module is ready the other modules aren’t. The problem with modules is you can’t have multiple versions of the same module in one project.

My problem is: I am not deciding when this module is ready, and as long as it is not published my contributions are kind of useless. So now I decided to write a module which adds functionality to the existing cbi18n module, so I can still use other modules such as cbvalidation and cborm as well. The interesting thing is: I can still use all goodies from the cbi18n module, and start using my own JSON based resources with all other improvements.

So let’s go back to this new module. How can it replace existing functionality in cbi18n, and what’s actually new?

Replacing the ResourceService by a custom ResourceService

The cbi18n module has an interesting feature: a customResourceService setting. I can inherit from the base resourceService and override the getResource and loadBundle function. I will not explain the details here but just focus on the added functionality. By installing my module and change some simple settings for cbi18n it will be changed in a JSON capable resourceService instead of the oldfashioned java resources with tricky editors. JSON is easy, supported by many front- and backend frameworks, so it can be changed by anyone who can use a simple editor. The whole process is documented in the github pages. In this post I will focus on the differences with the default cbi18n module.
Just change the i18n settings in your coldbox config:

i18n = {
    localeStorage="cookie",
    customResourceService="cbi18n-json-resources.models.jsonResourceService",
    defaultLocale = "en_US"
}

and you are ready to use JSON resources. cbi18n-json-resources has it’s own modulesetting where you have to define the path to your resources, e.g

modulesettings = {
    cbi18n-json-resources: {
        defaultResourceBundle = {
            "filename" = "default.json",
            "resourceRoot" = "resources/lang/"
        },       
        resourceBundles = {
            someAlias = {
                filename = "myAlias.json",
                resourceRoot = "resources/lang"
            },
            anotherAlias = {
                filename = "sometest.json",
                resourceRoot = "resources/lang"
            }
        },
        // by default unknownTranslations will be logged
        logUnknownTranslation = true,
        // 
        unknownTranslation    : ""        
    }
}

You can define a default resource and/or additional files for extra resources. This is not different from the original module were you can access your resource like

buttonOkText = getResource("button.OK")
buttonOkText = $r("button.OK")
// or //convenient '@alias' to specify alias bundle
buttonOkText = getResource("button.OK@someAlias")

JSON format: flat vs nested

You can use two different formats for your JSON resources, flat vs nested keys. The module will autodetect which format you are using.

//flat
"buttons.OK"        : "OK",
"buttons.Cancel"    : "Cancel",
// vs nested
"buttons" : {
    "OK"        : "OK",
    "Cancel"    : "Cancel"
}

Default and hierarchical resource files

There are two main reasons why I wanted to write this module: default resource files, and hierarchical files. This might need some explanation.

Most locales look like this: nl_NL, nl_BE, en_US, en_GB or even lang_COUNTRY_variant. Most of the time you will see locales consisting of a language part and a COUNTRY part which is the ISO country code. When coding new localized applications you often have to write for ONE language, but multiple countries. Countries might differ in date and number formatting, but in language there often are only small differences. If I wanted to provide locales for Dutch in the Netherlands (nl_NL) and Flemish in Belgium(nl_BE) I would have to provide ALL translations for both locales in the base cbi18n module. This can add up quickly for languages with many different country variations. It makes a lot more sense to provide a common language resource and only provide the different translations for specific countries (e.g US vs GB english). This is why we have hierarchical resources now. For the dutch example: I have the following files

  • resources/lang/nl/validation.json (all dutch translations)
  • resources/lang/nl_BE/validation.json (specific flemish translations, fallback to nl

So if I specify Flemish as a locale, it will load the nl resource first, and merge it with the nl_BE translations.

This brings me to the second requirement: default resource files. Especially when building a localised module, not all translated resources might be present from the beginning, because you still have to find translators, just added new functionality and so on. Erroring on missing translations is not always an option. Sometimes a default translation in some other language might be a better option (although completing your translations is even better 🙂 ) . So let’s say I want to build another localised module where I want to provide English as a default language and provide Spanish, French, German and Dutch as additional languages. In this case I can add the following resource files

  • resources/lang/default/validation.json (my default english resource file with all en_US translations)
  • resources/lang/nl/validation.json (dutch translations)
  • resources/lang/nl_BE/validation.json (specific flemish translations, fallback to nl and default)
  • resources/lang/en_GB/validation.json (specific british translations, fallback to default)
  • .. more locales…

By providing a default (english in this case) I open up this module for any additional locale, language or country variation. You just have to provide the translation keys and if they are not present there will be a fallback to english.

Interceptor for missing translations

The cbi18n has facilities to log missing translations. One of my customers had some additional wishes: logging these translations to sentry. This is quite hard (if not impossible) to achieve with the cbi18n logging setup, so an interception point was added. By writing your own interceptor you can do anything you want with your missing resource keys.

Conclusion

The cbi18n-json-resources module addresses some wishes I had for cbi18n. As long as there’s no update for cbi18n I can use this module for hierarchical JSON resource without giving up compatibility with cbi18n.

And for those of you wondering if I really wanted to write a module with english locale and Spanish, French, German and Dutch as additional languages: Yes! And it will be ready soon, so if you have seen my ITB2020 talk, you will know what to expect.