Multi-lang website with some nice features

Post links to sites running CMS in all its glory.
Locked
User avatar
velden
Dev Team Member
Dev Team Member
Posts: 3483
Joined: Mon Nov 28, 2011 9:29 am
Location: The Netherlands

Multi-lang website with some nice features

Post by velden »

https://tinyurl.com/y8ja4u3p

A little background information:

This website is private project of mine. Initially setup about 9 years ago because a site like this, for the Belgium stock index BEL20, didn't exist. That time I programmed the website logic from scratch using php and mysql for the database. The website serves some Google Adsense ads (no, I can't quit my day job yet ;-))

Although the frontend worked fine, the backend... well, actually phpmyadmin was my backend. The website was lacking a template engine like smarty, so updating the layout/design was a risky job because of the fact that the html was mixed with the php code.

After all these years I finally found the time (and courage) to migrate everything to CMS Made Simple.

Below I will share a little about some methods I used to get things working like I want it to.

My wishlist:
  • Easier to enter/alter data
  • Multi-language support
  • Possibility to add more stock indexes
  • As little duplicate data entry as possible
  • Obviously powered by CMSMS
  • ...
I bought the theme via Themeforest a few years ago. Currently the site serves two languages; English and Dutch. Adding a new language is rather easy but it should be noted that I'm the owner and only editor/admin of the website. So I didn't bother about editor-friendly ways to add a language. It will initially involve some manual actions and configuration but it can be done in a few minutes (apart from all the translation work that should be done of course).

LISE

The main module used is LISE. It currently is the only 3rd-party module installed.

Using LISE I created three LISE instances (a LISE instance actually is a separate module which you can easily extend with many different types of field definitions); one for the list of Companies (categorized by stock index), one for Dividends, and one for Agenda items.

LISE features a very useful API and events (check Extensions > Event Manager). I'll give an example from my website:

Dividend has 3 relevant dates (in my case): date of approval, date of ex-coupon and date of payment. I also want to show those dates/events in the website's agenda which holds other (non-dividend) events too (agenda items 'live' in another LISE instance, remember). So when I save a new (or edited) dividend item I use a UDT (User Defined Tag) connected to the PostItemSave event to automatically generate the three agenda items (obviously the dates need to be filled in the dividend item). The agenda item properties will automatically be populated: the date, the type of event (payment,approval or ex-coupon), the company etc.

The example above is only one of the events and UDT's I use. In real I created a few UDTs and plugins to do all the stuff that needs to be done. A little knowledge about PHP and how to use an API is required to set it up like this.

Multi-language support

For the multi-language implementation I'm using some custom plugins (can be UDTs too), .htaccess rewriting and Smarty config files (more about that later).

The page hieracrchy is based on the language codes for the root parent's aliases:
dpv3_ml_setup_01.png
Page template should check what is their root parent's alias. Check Rolf's blog for a method how to get it: https://www.cmscanbesimple.org/blog/mul ... page-alias

Note the url column; you can manually assign an url to a specific content object (e.g. a content page). This allows you to use a consistent naming scheme for the content aliases (language specific suffixes: contact-nl, contact-en etc.) and still use the desired url.

When the template 'knows' the root page alias it can be used to do useful things some of which are explained below.

But first a very simple auto detect language method implemented in .htaccess rewrite rules. I only use it to redirect vistors who are browsing to the main domain to a specific language. Note that French is commented out because it's not yet implemented. The rules are very simple: if the browser sends a Accept-Language header AND it starts with 'fr' (todo) or 'en' the visitor will be redirected to /fr or /en. In all other cases visitors will be redirected to /nl

Code: Select all

	#RewriteCond %{HTTP:Accept-Language} ^fr [NC]
	#RewriteRule ^$ /fr [L,R=301]

	# English
	RewriteCond %{HTTP:Accept-Language} ^en [NC]
	RewriteRule ^$ /en [L,R=301]

	# else redirect to the Dutch version
	RewriteRule ^$ /nl [L,R=301]
Smarty config files

Smarty config files allow you to create a plain text file to assign strings to variables. I'm using sections for the languages (but you can use separate files per language too if you want). Example config file (snippet):

Code: Select all

#global variables
#if a variable also exists in a section below that is loaded, it will be overwritten by the value in that section

url_twitter=https://www.twitter.com/whatever
url_facebook=https://www.facebook.com/whatever

### DUTCH ###
[nl]
amount=bedrag
date_approval=datum goedkeuring
date_excoupon=datum ex-dividend
date_payment=datum betaling
...

### ENGLISH ###
[en]
amount=amount
date_approval=approval date
date_excoupon=ex-coupon date
date_payment=payment date
...

### FRENCH ###
[fr]
amount=montant
...
(Note that you can use it for other things than translations too)

Because of variable scope issues I load the config file and assign all variables to a Smarty array. If the visitor visits an English page the 'en' section is loaded. For Dutch pages the 'nl' section etc. Because of this method I don't need to create separate templates for each language. In smarty I can use the same variable names and the right text will be shown:
(the array with the variables from the config file is named $l)

Code: Select all

<dl class="dl-horizontal"> 
	 <dt>{$l.amount} <i class="fa fa-eur text-primary"></i></dt>
	 <dd>{$div->fielddefs['amount']->value|string_format:'%.3f'}</dd>
	 <dt>{$l.date_approval} <i class="fa fa-legal text-primary"></i></dt>
	 <dd>{$div->approvaldate|cms_date_format}</dd>
	 <dt>{$l.date_excoupon} <i class="fa fa-scissors text-primary"></i></dt>
	 <dd>{$div->excoupondate|cms_date_format}</dd>
	 <dt>{$l.date_payment} <i class="fa fa-money text-primary"></i></dt>
	 <dd>{$div->paymentdate|cms_date_format}</dd>
</dl>				 
For a Dutch page the above will output:

Code: Select all

<dl class="dl-horizontal"> 
	<dt>bedrag <i class="fa fa-eur text-primary"></i></dt> 
	<dd>2,000</dd> 
	<dt>datum goedkeuring <i class="fa fa-legal text-primary"></i></dt> 
	<dd>apr 25, 2018</dd> 
	<dt>datum ex-dividend <i class="fa fa-scissors text-primary"></i></dt> 
	<dd>apr 30, 2018</dd> <dt>datum betaling <i class="fa fa-money text-primary"></i></dt> 
	<dd>mei 3, 2018</dd> 
</dl>
For an English page:

Code: Select all

<dl class="dl-horizontal"> 
	 <dt>amount <i class="fa fa-eur text-primary"></i></dt>
	 <dd>2.000</dd>
	 <dt>approval date <i class="fa fa-legal text-primary"></i></dt>
	 <dd>Apr 25, 2018</dd>
	 <dt>ex-coupon date <i class="fa fa-scissors text-primary"></i></dt>
	 <dd>Apr 30, 2018</dd>
	 <dt>payment date <i class="fa fa-money text-primary"></i></dt>
	 <dd>May  3, 2018</dd>
</dl>
Obviously it is convenient to use this method in the whole website; determine the language of the page (based on directory/url/root parent) at the very beginning and load the proper section from the config file. Because it's a file it's not easy to edit from within CMS Made Simple. Hence I use it for a fixed set of variables. If you want to use a config file like this you can save it in the /assets/configs/ folder.
It's also nice to use during development because you can start with defining variables at the top (above the first section). Those variables will be loaded regardless of the chosen section. If the website is almost finished you can start moving (or copying) those variables to the language sections and do the translations. I choose to prefix the VALUES of the top variables with an underscore so when checking the frontend I easily noticed if I forgot to translate a variable.

If you need to use a translated string that holds a variable value itself (e.g. a company name) it's possible to use it together with the php function sprintf.
Example:

Code: Select all

[en]
...
company_meta_description=Detailed information about the dividends of %s
Inside a company detail template:

Code: Select all

  {$page_description=$l.company_meta_description|sprintf:$item->title scope=global}
Final result in the meta tag:
<meta name="description" content="Detailed information about the dividends of Proximus">
'%s' will be replaced by the value of the title property of the Company item in this case.
Note: to use the sprintf function you may need to loosen the security configuration for smarty!

I use only one LISE instance per language, so e.g. a Company item has an information text field for Dutch, one for English and one for French. All those fields are consistently named by language(e.g. info_nl, info_en, info_fr). Again to be able to use one Smarty template for all the languages:
dpv3_lise_backend_ml_01.png
Example snippet from a LISE detail template

Code: Select all

{* $lang holds the language code (nl, en or fr) and is globally defined at the beginning of the main page template *}
<h3>{$l.company_information}</h3>{* <-- print the header *}
{$item->fielddefs["info_$lang"]->value}{* <-- dynamically get the proper field from the LISE instance *} 
Above method is fine for fields which will be filled for all languages at all times. In my case I sometimes have items which only have filled one language field. Because I rather show to the visitor information from another language than no information at all (many Belgium/Dutch people will be able to understand English for example) I wrote a small plugin which takes the specific item and checks whether the preferred language fields are filled. If not, it will check for another language. Finally it returns an array with values (the information) and language codes.

This way it is possible to:

- inform the visitor that a specific piece of information is offered in another language
- set a lang attribute so browsers/search engines know about this different language
dpv3_auto_lan_warning_01.png
dpv3_auto_lan_warning_01.png (9.92 KiB) Viewed 6948 times
dpv3_proper_lang_attr_01.png
dpv3_proper_lang_attr_01.png (6.55 KiB) Viewed 6948 times
To get language specific urls I use a little code to search and replace some parts of the generated LISE urls. E.g. for Dutch it can be /nl/bedrijf/bel20/ab-inbev-nv/ and for English /en/company/bel20/ab-inbev-nv/.
In .htaccess I created some rewrite rules to rewrite those 'hand made' to urls CMSMS and LISE do understand. The rewrite rules contain (hard coded) the page id of the language specific target pages (LISE needs to know on which content page to display the item, and it's different for every language).
Note: If you want to have language specific aliases for an item I think you can't use this method but should create LISE instances for every language. Another advantage would be you can define the language specific url per LISE instance and set the target page there too. I think that is a better way to do it, so perhaps I will change it (in 9 years from now :-) )

Navigation / Menu

To get the LISE items (companies) in the Navigator menu I used another custom plugin. If the Navigator template comes across a node (content page) with a specific value for extra1 (read about it here and in the help of the Navigator module) it will call the plugin which return 'nodes' based on the LISE company items (one of the extra page attributes holds the wanted LISE category). Those items will be used to create the submenu with companies.
Another little trick I used here is to add to the top of this submenu the parent page. The 'place' of the parent page itself is turned into a navigation 'header'. This is mainly useful for touch devices so they will be able to expand the menu without navigating to another page immediately.

Admin links on the frontend

Because the LISE instances (especially for dividend en agenda) each contain several hundreds of items it sometimes is hard to find a specific item in the backend list. Further, it sometimes happens I see missing information or something changes (like a date of an agenda item) and want to change it immediately . So again two plugins:

One checks if the website visitor happens to be logged in in the backend too (same browser). I just check for logged in because I'm the only admin user but it would be possible to check for specific group membership too.
Another plugin creates urls to specific admin actions of modules. Have a look at the screen shot below. Those red and orange buttons are only available when I'm logged in in the CMSMS admin backend too. The links will bring me immediately to the specific item to edit it (or add or delete). It even 'knows' when a agenda item is automatically created (as explained before) by creating a dividend item so in that case it will bring me to the dividend item to change it there (for consistency reasons).
dpv3_admin_urls_01.png
It seems to work rather well but it should be noted that sometimes it does not. Probably when there has been a longer period of inactivity in the backend. I need to do some research but I guess that the backend session has expired then. Just a click in the backend and refresh of the frontend and things work well again. Though I wouldn't recommend to offer this option to regular customers like this yet.

Conclusion

First of all I'm happy with the result. Now it's much easier to add and edit my items and multi-language support has been added. This motivates me to update the information more often.
I needed to create a handful of custom plugins and UDTs to do some tasks (some of them are ok, some I'm not proud of :-)).
Everything is done without changing a single bit of code of the core nor the modules (so no hacks). Though I'm using a few database queries (to LISE tables) to make some processes more efficient than possible with the default options available.

The site isn't finished yet, I have some ideas to extend it, but the first, main step has been set (implementing CMSMS).

Hopefully some of you find the above description useful and of course it's an invitation to share your work, methods etc too.
User avatar
velden
Dev Team Member
Dev Team Member
Posts: 3483
Joined: Mon Nov 28, 2011 9:29 am
Location: The Netherlands

Re: Multi-lang website with some nice features

Post by velden »

A small update:

After writing above post yesterday I decided that the use of one LISE instance for all languages was a wrong decision for the 'Companies'. Companies have a name, an alias and an url which should be able to hold different values per language.

So in stead of waiting another 9 years I decided to change it this weekend. Every language now has it's own LISE company instances.

This made the url stuff easier and allows for 'localized' name, logo etc.
Companies now have multiple Edit buttons too
Companies now have multiple Edit buttons too
As the LISE agenda items and LISE dividends don't have their own detail views (they are always displayed in a summary action/template) they don't need localized urls, aliases etc. So those two modules will keep all information for every language in one single instance.

Another advantage of keeping languages separate can be delegation. If you have multiple editors you can give them permissions to only edit a specific instance.

In my case it is not needed.
dpv4_multi_instances_01.png
User avatar
Gregor
Power Poster
Power Poster
Posts: 1874
Joined: Thu Mar 23, 2006 9:25 am
Location: The Netherlands

Re: Multi-lang website with some nice features

Post by Gregor »

Impressive job you did Velden. My compliments!
Locked

Return to “CMS Show Off”