Integrating i18next framework. Translations and language detection covered

So I finally finished my i18next integration. It’s not a native integration, but you can basically copy and paste my code and it will work for you.

All my i18n needs are now covered within Wappler and now yours can be to.

Instead of writing 2000 words and pasting 50 screenshots(I’m looking at you @Teodor) I will upload a quick video and attach the files so you can see what is actually happening. The source code is fully commented but if you have any doubts feel free to ask.

Note: Don’t freak out if you don’t see an Italian translation. It’s on purpose.

Just unzip this guy below and pop the 3 files in a blank Wappler project and you can learn with the comments and the code.

i18ntest.zip (4.1 KB)

The whole file has less than 100 lines of code and the actual integration has less than 30 lines. The rest is code for the example web I showed in the video.

And remember that you have all the i18next documentation at your disposal. What works with vanilla i18next should work with this integration.

You just have to remember that there is only one change when you call a string to be translated.

In vanilla i18next you translate a text using this code

i18next.t('key', options)

In Wappler if you follow my integration you use a custom formatter.

{{ "key".t(options) }}

That way you can chain App Connect formatters so you could do

{{ "key".t(options).uppercase() }}

You would get the translated string for “key” in uppercase.

Enjoy.

Disclaimer: This is just a basic example. You will need to adapt, add and/or remove some things to make it work with your project. If your users can store their language preference in the database you will have to retrieve that value and pass it as an option. You may not want to default to browser language on load and make the user select. Or you may want to always present english and then give the option to change.

24 Likes

Hi Jon: A few questions. I preface by saying that I am a novice developer so bear with me. I visited the i18next site. What I observed was that translations are manually created in the code. This would be a solution if you site were static. Is that it’s only application? Can this be implemented on a dynamic site? And does i18next have a library of foreign languages lot access?

I also visited a site named locize.com that was referenced in i18next. It appears, for a fee, that they will translate the site on the fly. Are you integrating their technology on your site? Do they provide the translations and i18next just serves as a platform?

Hi Bruce,

With i18next you can add translation strings locally in the code or you could use a backend plugin to retrieve them. In my example, I am using the http backend plugin which will make an http call to retrieve json files.

That’s a common misunderstanding with dynamic/static sites. Dynamic doesn’t necessarily mean having a database behind. A static website will deliver it’s content as stored. A dynamic website will deliver content based on certain logic.

So just by adding i18next would make our website dynamic.

It’s main application is to make the browser render text strings based on language through a set of “helpers”. These helpers will make your life easier when dealing with idiomatic specifics.

This is going to be hard to explain but I’ll do my best. I will assume you are referring to a website that reads part of its content from a database.
Databases should be used to write, store and read raw data. Content in our context. That content can be stored in different languages, but it shouldn’t contain idiomatic logic.

By idiomatic logic I mean specifics that differentiate languages like plurals, gender, pronouns, etc
Idiomatic logic is handled by i18next through the helpers because that’s the difficult part of i18n.

Retrieving translated content from a database is the easy part and you actually don’t need i18next for that. Wappler will retrieve your translated database content by just passing the language via the DB query. That content as a block is called dynamically by the website, but the content itself is static in nature.

Let’s say you have a web app for users to read public domain books in different languages.
You would use i18next to translate the actual web interface and Server Connect to retrieve the books in the language requested. But the book is already translated as a whole. There is no idiomatic logic in it because the book is static.

Same should go for any other type of content stored in the database. You store raw data. Not logic.

No. i18next will not translate for you. You could easily “invent” a language tomorrow and use this framework so it delivers a user interface based on your language. But you still need to translate the strings for your UI and “Don Quixote” to Klingon and store it in the database if you know what I mean.

From what I understand locize is a Translation Management System, a CDN to deliver language strings and a backend plugin for i18next framework. They will not translate for you.

The creators of i18next framework are the same of locize. I think it’s a great business model.

I hope it all makes sense.

1 Like

Modularized approach and remember language preference.

Create a SSI php include file and drop this in it:

<!-- Wappler include head-page="" appconnect="local" is="dmx-app" components="{dmxDatastore:{}}" -->

<script src="../../dmxAppConnect/dmxBrowser/dmxBrowser.js"></script>
<script src="../../dmxAppConnect/dmxDatastore/dmxDatastore.js" defer=""></script>
<script src="../../dmxAppConnect/dmxFormatter/dmxFormatter.js" defer=""></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/i18next/19.4.4/i18next.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/i18next/i18next-http-backend@1.0.8/i18nextHttpBackend.min.js"></script>

<script>
	var translationLoaded = false
	var callback = function(){		
		i18next
			.use(window.i18nextHttpBackend)
			.init({
				//Our website will only support english and spanish for the time being			
				whitelist: ['en', 'es'],
				// Look for language setting in local storage. If not found use browser language.
				lng: dmx.parse("i18n.data[0].language") ? dmx.parse("i18n.data[0].language") : dmx.parse('browser.language.substr(0, 2)'),
				// If browser is not in english or spanish it will show english	
				fallbackLng: 'en',	
				// We load our strings from here. It will take the name of the page as the name of the file.		
				backend: { loadPath: '../../assets/translations/{{lng}}/'+dmx.app.name+'.json'}
			})
			.then(updateDOM)
		}	
		if (document.readyState === "complete" || (document.readyState !== "loading" && !document.documentElement.doScroll)) {
			callback();} 
		else {
			document.addEventListener("DOMContentLoaded", callback);}

		//We added a condition to make sure the translation function is not called before the translation is available. Avoids some console warnings.
		dmx.Formatter('string', 't', function (key, options) {
			if (translationLoaded)
				return i18next.t(key, options);
			else
				return key
		})
		
	//Reevaluates all the App Connect bindings and updates contents of the page based on language
	function updateDOM() {
		dmx.requestUpdate()
		dmx.global.set('translationLoaded', true)
		dmx.global.set('translationLanguage', i18next.language)
		dmx.parse("i18n.upsert({$id: 1, language: translationLanguage},{language: translationLanguage})")		
		translationLoaded = true
	}
</script>

<div is="dmx-browser" id="browser"></div>
<dmx-datastore id="i18n"></dmx-datastore>

Remember to change loadPath value if you want to load json translation files from a different place.

The code above will take the files from /assets/translations/{{lng}}/{{pagename}}.json

So if your page is named dashboard and french is selected and supported(not in our example) it it will attempt to download /assets/translations/fr/dashboard.json

Once you have this SSI PHP include file configured as per your needs you just need to call it from each single page you want to translate by adding <?php include '../includes/i18n.php'; ?> to the body.

image

Remember that the language chosen will be the one stored in local storage or browser language the first time you load the page.

If you want the user to be able to select the language and the browser to remember the selection just run i18next.changeLanguage('es').then(updateDOM)

I am currently using a footer and a dropdown for this.

<div class="dropdown-menu dropdown-menu-sm dropdown-menu-right">
     <a dmx-bind:href="browser.location.pathname+'#'" class="dropdown-item" onclick="i18next.changeLanguage('en').then(updateDOM)"><img alt="English" src="../../assets/img/icons/flags/en.svg" class="mr-2">English</a>
     <a dmx-bind:href="browser.location.pathname+'#'" class="dropdown-item" onclick="i18next.changeLanguage('es').then(updateDOM)" data-toggle="none"> <img alt="Spanish" src="../../assets/img/icons/flags/es.svg" class="mr-2">Spanish</a>
</div>

This will change the language to one of them and call the updateDOM function that will reevaluate all AC bindings, update the DOM and store the selected language in the data store component so it’s remembered next time the user visits the website.

If you have a user database and you want to store their preferred language in the database you could easily sync the datastore language setting with the database user preferred language each time the user logs in or changes the setting in his account/profile settings. But this is not covered in this example.

This new example was to modularize our i18nextr integration and store language selection in local browser storage so it’s remembered.

Remember that there will still be a flickering if you do not hide body until translations are available.

<body class="application application-offset" is="dmx-app" id="dashboard" dmx-show="translationLoaded">

Note: If you use this approach remember you don’t have to call these 3 App Connect libraries in the pages you add this SSI PHP include file as they are already called by the SSI.

<script src="../../dmxAppConnect/dmxBrowser/dmxBrowser.js"></script>
<script src="../../dmxAppConnect/dmxDatastore/dmxDatastore.js" defer=""></script>
<script src="../../dmxAppConnect/dmxFormatter/dmxFormatter.js" defer=""></script>
5 Likes

Hi Jon,

Let me start by saying thank you for the detailed explanation you provided in your response. I am sure that other developers will bookmark this for future reference. I will try this out in a test environment to become familiar with the concept. Thank you for getting me started on this path. Bruce

1 Like

Do you also have message like :

Formatter t in expression ['simplekey'.t()] doesn't exist for type string

That’s caused because at the time of App Connect evaluating your translation the formatter is not available.

1 Like

How to make it available ?
I tried to put the script on the top, same effect …
Also I’m trying your ssi solution, will tell in few minutes if this one works for me

Probably its this because the error occurs only the first load, changing language no more error …
Maybe starting the translation on a ready function.

That’s already handled by:

var callback = function(){ i18nextinitialization }	
		if (document.readyState === "complete" || (document.readyState !== "loading" && !document.documentElement.doScroll)) {
			callback();} 
		else {
			document.addEventListener("DOMContentLoaded", callback);}

Hey @JonL,

Thank you for sharing this integration framework. I implemented it and it works fine on static text, however does it work on dynamic text as well?

I am trying to translate some text that comes as json response from an API call on server connect, and I am not able to do it.

For example I have this code snippet :

<div class="col-sm-4 font-weight-bold">{{'page.Status'.t()}}</div>
    <div class="col-sm-8">
    <p dmx-text="sc_getApp.data[0].Status"></p>
</div>

Do you have any idea how I can get it working?

This i18next implementation will only work for static.

However I am puzzled. Does your API always return the same information so you can anticipate the strings that need to be translated?

The info returned by the API can differ slightly, in the example above, I have a few options: Booked, completed, confirmed which can differ based on the record id selected. do you think it can work in this case?

Yes. It should be possible. Try running a dmx.requestUpdate() once you have retrieved the info from the API.

You can try running it from the console to check if it gets translated before working on the final implementation.

I just tried and got undefined…

Shouldn’t i wrap the response in something similar to this {{‘page.Status’.t()}} and then maybe run the dmx.requestUpdate() ?

Try with {{page.Status.t()}} without quotes as I believe it’s a variable, right?

Actually I managed to get it translated using the following method:
<p>{{('page.'+sc_getApp.data[0].Status).t()}}</p>

Thank you, now I am trying to get the same thing done for some notifications I have on the page that run on server connect events like this:

dmx-on:error="notifies1.warning('(page.Error loading details'.t()))"

However its not working, do you know in what format it should be written ?

The quotes and parenthesis seem to be wrong in your expression.