Translatable and the URL (new)

Translatable and the URL: language prefix

Encouraged by some clever code, sent to me by smares (thanks for that!), I decided to again try my luck in extending ModelAsController to enable the use of a language prefix in the URL for Translatable sites. And as far I can see, it is now working nicely! There's a downloadlink below, but you might read this first...

So what are we aiming at now?

  1. Every url should have a locale prefix, as in http://mydomain/en/myenglishpage and http://mydomain/nl/mydutchpage
  2. The homepage urls should look like http://mydomain/en/ and http://mydomain/nl/
  3. http://mydomain/ should be redirected to http://mydomain/nl/ (the default locale)
  4. The prefix should be customizable: en, eng, en_US, all should be possible
  5. Suport 301 redirects of bookmarked pages that have since been renamed/relocated
  6. Suport Static Publisher, using the language prefix in the cache
  7. Page links should automatically include the language prefix
  8. Links to the Homepage should lose the URLSegment for every language
  9. Language switchers using ?locale=.. should still be supported

Creating the links

in your Page class add:

/*
 * language prefix for translatable sites
 * format: locale => prefix  
 */
static $translatable_prefix = array(
	'en_US' => 'en'
);


/*
 *  add the prefix to your links
 */
function Link() {

	if(!Translatable::is_enabled()) {
		return parent::Link();
	}

	$link = $this->RelativeLink();
	
	// remove the $URLSegment for any homepage. Mind: this will only work if 
	// a home page is (a translation of) the master homepage (as it should really be)!
	if ($link != '/') {
		$HomeSegment = Translatable::get_homepage_link_by_locale($this->Locale);
		if ($this->URLSegment == $HomeSegment) $link = '/';			
	}

	return Controller::join_links(
		Director::baseURL(), 
		self::$language_prefixes[$this->Locale], 
		$link
	);
}

This will provide the flexibility to select whatever prefix you want and create the proper links in your websites navigation. In fact this goes for all situations described above. You can then set Page::$translatable_prefix = array( ... ); from your config.

in /mysite/_config.php add:

// set the default language for a multilingual site
Translatable::set_default_locale('en_US');

// Define which objects you wish to include in your translations
// It can be any DataObject, requires /dev/build??flush=1
Object::add_extension('SiteTree', 'Translatable');
Object::add_extension('SiteConfig', 'Translatable');

// Format: locale => prefix
Page::$language_prefixes = array(
	'nl_NL' => 'nl',
	'en_US' => 'en'
);

TranslatableModelAsController

By default, every URL will be handled by ModelAsController - the spider in the web that will ultimately decide which controller should handle which page(type). There is a default rule in /sapphire/_config.php that will initially hand everything over to this controller - unless a rule with a higher priority is found that fits a specific url. To take the language-prefix. and everything that comes with it, into the equasion we need to extend ModelAsController. The new default controllerclass will be called TranslatableModelAsController, and we need to create a new 'general' rule for it.

In /mysite/_config.php add:

Director::addRules(10, array(
	'$Locale/$URLSegment//$Action/$ID/$OtherID' => 'TranslatableModelAsController',
));

The priority is set to 10. Note that setting it any higher will have it overrule other specific rules. This might make specific URLs like /admin/ inaccessible, so beware!

Download TranslatableModelAsController

Now for the new controller: I've tried to stick as close to the ModelAsController as possible in the way the URL is interpreted. As it's a bit long, I'll just make it downloadable here. With a bit of extra explanation below. Any comments or bugreports are welcome.

» Download TranslatableModelAsController.zip (09-01-2012)

TranslatableModelAsController explained...

The basic function is getNestedController(). It will do the following:

1. Set the Locale
Try to set the Locale from the language prefix (function setLocale). If no proper locale is found, a 404 page is generated (showPageNotFound()). The latter function is somewhat more complex then usual: if the provided locale doesn't exist, or no 404 error page has been created for it, it tries to show the 404 error page for the default locale. If that cannot be found the 404 whitescreen is generated - aaargh...

Note: Setting the errorpage to the default locale means you need to get the Translatable::$default_locale value, but unfortunately no getter for it is provided in Translatable yet, even though everything else is deprecated... Hence the construction with Translatable::get_default_lang()

2. No $URLSegment?
This means we request the homepage for the current Locale is requested. Get the proper URLSegment for it from Translatable::get_homepage_link_by_locale($this->locale). This will work if the homepage is either the master (default locale) or a translation. If no homepage can be found, try to get the first page in the siteTree for the current locale. If there is none, then just redirect to the default homepage.

Note: having a 'rogue' homepage like that means the PageLink will not be abbreviated to '/'. So in your navigation the link for this page will be set to its proper $URLSegment.

3. We have a URLSegment?
Try to find the page. The only difference with the original ModelAsController is that the locale_filter is now enabled. This means the Page can only be found within the current locale.

4. No page found?
It might be renamed or relocated. Try to find it within older versions. If it exists, then redirect to its current name/location. Again, almost equal to the original function. The main difference is that the original ModelAsController::find_old_page() function uses a SQLQuery object to perform a query - and it doesn't apply the locale filter! So I had to replace the entire static function - to make sure only old pages within the current locale are returned: TranslatableModelAsController::Asfind_old_page_localized().

5. still no page found?
Generate a 404!! 

Homepage redirect issue

Whenever the default homepage is called by its URLSegment (as in mydomain.com/home/) SilverStripe will automatically redirect to the root of the site. That is not what we want when using a language prefix! What's more: when a Translated homepage is requested this way, SilverStrip won't redirect at all. What we want is that every homepage called by their URLSegment, gets redirected to its own prefixed 'root'. Now this is the most complicated part of all - and I'm not quite sure how to approach this. The default redirection takes place in the ContentController::int() function:  

public function init() {
	parent::init();
	// If we've accessed the homepage as /home/, then we should redirect to /.
	if($this->dataRecord && $this->dataRecord instanceof SiteTree
			&& RootURLController::should_be_on_root($this->dataRecord) 
			&& (!isset($this->urlParams['Action']) || !$this->urlParams['Action'] )
			&& !$_POST && !$_FILES && !Director::redirected_to() ) {
		$getVars = $_GET;
		unset($getVars['url']);
		if($getVars) $url = "?" . http_build_query($getVars);
		else $url = "";
		Director::redirect($url, 301);
		return;
	}
	...

This conditional uses the RootURLController::should_be_on_root() function to check if it should redirect. Unfortunately this only works for the default homepage, and the redirect goes straight to the root! We can extend the init() function in the Page_Controller, and create a similar conditional for the new situation. But unfortunally, to avoid the original redirect, here we cannot call parent::init at the strt of the function. And if we call it at the end of the function, we'll get the error message  that parent::init() is not called - even if the condition is not met, and I can't figure out why yet!

And even after the good redirect, the 'bad' redirect still happens for the homepage... So I decided on a small 'hack' to avoid the 'bad' redirect by adding an action 'Index' if no action exists. This now works without any core changes.

I still needed a check on should_be_on_root(), so I deviced a similar function TranslatableModelAsControler::should_be_on_root() that will check every homepage, not just the default one. So here we are:

In the Page_Controller add:

/*
 *  If we've accessed the homepage as prefix/URLSegment/, then we should 
 *  redirect to /prefix/
 *  Also, to avoid redirecting the default homepage to the site root, set 
 *  the action to 'Index' if it's empty, as Idex() is called by default anyway...	
 */
protected function CheckTranslatableHomepage() {

	if(Translatable::is_enabled()) {

		if($this->dataRecord && $this->dataRecord instanceof SiteTree
			 	&& TranslatableModelAsController::should_be_on_root($this->dataRecord) 
				&& (!isset($this->urlParams['Action']) || !$this->urlParams['Action'] )
				&& !$_POST && !$_FILES && !Director::redirected_to() ) {
			
			$getVars = $_GET;
			unset($getVars['url']);
			$prefix = Page::$language_prefixes[$this->Locale] . '/';
			if($getVars) $url = $prefix . "?" . http_build_query($getVars);
			else $url = $prefix;
			Director::redirect($url, 301);
			return;
		}
		// prevent the default homepage from redirecting to the site root...
		if (empty($this->urlParams['Action'])) $this->urlParams['Action'] = 'Index';
	}
}

Now the only thing left is to add this to the Page_Controller::init() function:

public function init() {
	$this->CheckTranslatableHomepage();

	// other code goes here...

	parent::init();
}

Note: I've not yet found any issues with that yet, but if you do or you can think of a better way, please tell me...

Redirect the websiteroot

If you want, you could do a 301 redirect for the homepage from / to /en/, to avoid duplicate content, but you don't really need to, as the new CheckTranslatableHomepage() in the Page_Controller should take care of that.

.htaccess:

# redirect the the homepage.
RewriteRule ^/*$ http://domain/nl/  [R=301,L]

Lighttpd:

# redirect the homepage root to /nl/
url.redirect = ( "^/$" => "http://mydomain/nl/" )

Static Publisher

This version works fine with Static Publisher, creating a folder for the lanuage prefixes and placing all html files in the proper folder - except for the homepages: they will be stored in the cache root as en.html and nl.html. But somehow I kind off like that, so I'll leave it at that.

@TODO

Think of a way to make this a proper module..?

Comments

  • ...and I just couldn't find it :-( Thanks!

    Let me know about any bugs you come across, as this is still very new...

    Verstuurd door Martine, 10/01/2012 6:00pm (5 jaar geleden)

  • Awesome work, looking forward to using this the coming days. One thing though: you mention that there is no getter for default_locale - there is! :) Translatable::default_locale() in line 259.

    Verstuurd door smares, 10/01/2012 5:01pm (5 jaar geleden)

Het versturen van reacties is uitgeschakeld.

RSS feed voor reacties op deze pagina