Jump to content

Multilanguage URLs / alias creation


Recommended Posts

Hello everybody,

I want to use one tree for different languages - as proposed in the API section - with a prepended /en/ oder /de/. That works fine. But is there a way to define an alternate "name" field for each language? Should work the same as with title and any other language_alternate field. Goal ist to have domain.com/en/products/productA in english and domain.com/de/produkte/produktA in german for example. How can that be accomplished?

Thx in advance for suggestions!

Marc

Link to comment
Share on other sites

Are you talking about example 3 from this page? There won't be a simple way to use different page names there since one page can't have multiple names. You could always add your own field to the page to hold it's name in other languages, and have your language-gateway template attempt to load them by that field. But I think that in the case you are describing, multi-tree would be preferable.

Link to comment
Share on other sites

Hey Ryan,

thanks for your answer. I think the biggest benefit of the alternate fields is to handle a website with one tree. That is always preferable if you have a 1:1 translation of most of the pages in your site where not all of the fields need to be translated. But of course it is always nicer to have "speaking" urls in each language (for SEO also as for users).

Your suggestion with an extra field would be straight forward, but beside the language gateway template I would also have to hack in the URL generation process I guess. Is there a better way to accomplish that than by doing some

If language == en then iterate over the path and use the alternate name field for URL creation?

How would a slim solution look like? I think this would be a usual requirement for "real" multilanguage websites and a huge advantage over concurrent systems.

Link to comment
Share on other sites

This may seem a bit hackish, but you could have multiple trees but keeping one main tree with all the fields of all languages. The other trees would be there only for constructing the urls. There doesn't have to be any field on their pages. The template for each of those pages can call the fields from the corresponding page from the main tree.

So, for having the kind of urls you want (domain.com/en/products/productA), on your template you could do this (not tested):

(--Assign this template to all the pages in the language trees--)

// Here I will assume that English is the default language, and the main branch of the tree

$de = $pages->get("/de/");
$pt = $pages->get("/pt/");
$parents = $page->parents; // An array with all the ascendants of current page

if ($parents->has($de) || $page->name == "de") { // If current page is under /de/
	$lang = "de";
}
if ($parents->has($pt) || $page->name == "pt") { // If current page is under /pt/
	$lang = "pt";
}

if (isset($lang)) { // If not the default language -> change from if($lang) to if (isset($lang)) to prevent errors

	// Change to the correct language
	$user->language = $languages->get($lang);

	// Now we have to convert the $page object in the correspondent page from the main tree
	// I'm using a pagefield now (just include a pagefield on the template and choose the corresponding page from the main tree)
	$page = $pages->get("$page->pagefield");
}
include("./{$page->template}.php"); // includes the original template file

On the head, change the navigation to: (here i used the default theme as example)

$homepage = $pages->get("/");
if (isset($lang)) $homepage = $pages->get("/$lang/");

Maybe this is confusing, and maybe not easy to deal with links later, i don't know... I will sleep and see if it still makes any sense in the morning :)

edit: I did some changes in the code, The most important are: using a select page field, including the original template, and fixing the navigation

  • Like 1
Link to comment
Share on other sites

Your suggestion with an extra field would be straight forward, but beside the language gateway template I would also have to hack in the URL generation process I guess. Is there a better way to accomplish that than by doing some

I think you'd make your own URL generation process. Lets say you've added a multi-language text field to your templates called 'url_name', you might do something like this, and call this URL() function rather than the url() function attached to every PW page:

function URL(Page $page) {
 $language = wire('user')->language; 
 if($language->isDefault()) return $page->url;
 $url = '';
 foreach($page->parents as $p) {
   $url = '/' . $p->url_name . $url;
 }
 $url = '/' . $language->name . $url . $page->url_name . '/';
 return $url;
}
How would a slim solution look like? I think this would be a usual requirement for "real" multilanguage websites and a huge advantage over concurrent systems.

I think that the slim solution is to use multiple trees. But I agree with the advantages of being able to specify page names in different languages, for the same page. I just don't currently know how to build that in without adding huge amounts of complexity to the system. It's something I will keep thinking about here in case any straightforward technical solutions become apparent.

Link to comment
Share on other sites

I think you'd make your own URL generation process. Lets say you've added a multi-language text field to your templates called 'url_name', you might do something like this, and call this URL() function rather than the url() function attached to every PW page:

So I call $page->URL in my templates and the navigation creation? Okay, sounds easy. But what about urls generated with TinyMCE for example? I could change the url module of TinyMCE of course. But is there perhaps a way to hook into the core url() function so I could just extend it with your function?

Thanks for your help Ryan, much appreciated!

Link to comment
Share on other sites

Hey marc, as Ryan already stated, changing this core functionality would require A LOT of changes, additional complexity... The ___path function of pages is certainly hookable and possible to rewrite urls, but then you'll have not resolving urls, and the journey continues. You'll certainly run into problems not being able to pull it off because it can't be done or has side effects.

As suggested using mutliple trees is the most slim solution. I'm thinking about this subject a long time, and refused to really ask for such a feature because I knew Ryan's answer already. :)

All projects I did at work are multilingual and I solved it always using multiple trees. It makes a lot of thing easier and you doesn't need to consider "will this work here" and start fighting the system. Also you are flexible as much as possible.

What I additionally tend to do, is to use "central" content where it makes sense (properties), that's put outside the language tree and then use urlSegments and the language information, to pull the content and display. Urls set in TinyMCE can also be parsed by a runtime hook after Page::render, you then search for certain url structure and replace it with the one you need. I've also done it with simple sections for a online shop (bag-shop.ch) where the products are outside the language tree and the collection page just renders them and product urls get rewritten using the hook technique described above.

Link to comment
Share on other sites

I tested the code that I posted above, and it works great :) I updated it with some changes that I did while testing.

The navigation works like a charm with this change.

Link to comment
Share on other sites

Thx for your suggestions :)

@diogo: I think a different tree just for the navigation is not very usable. You first have to create the content and after that create another page somewhere different. If the original page gets unpublished/removed/moved the structure breaks. I think the only possibility to deal with that is to create the navigation tree automagically with hooking after the page save/duplicate/move and populate the page in the navigation tree. That could work... and the user doesn't have to deal with a separate tree.

@soma: Hmm, where is the other language on bag-shop.ch? :-) As long as you are using the system on your own, a separate tree is okay. But I think it is hard to explain a client why all the belonging stuff shouldn't be in one place.

Link to comment
Share on other sites

I think the only possibility to deal with that is to create the navigation tree automagically with hooking after the page save/duplicate/move

I think this is how it works with Oliver's module. Also works like this with the repeatable fields. Shouldn't be too difficult to do it. They could even be hidden from editors.

I still have a problem with the links. Since I replaced the $page object by the original page, $page->children, gives me the children of the English page. It would be easy to solve by creating a myPage=$page instead of replacing $page. But would be nice to do all this without even having to change the already written templates.

Link to comment
Share on other sites

@MadeMyDay are you still struggling with this? I came up with a solution that uses url segments... of course you would loose this functionality on your templates...

You would only have to create an empty page for each language and assign it this template:

<?php
function toSlug($str)
{
   $str = strtolower(trim($str));
   $str = preg_replace('/[^a-z0-9-]/', '-', $str);
   $str = preg_replace('/-+/', "-", $str);
   return $str;
}
$lang = $page->name;
$user->language = $languages->get($lang);
$basePage = $pages->get("/");
$segments = $input->urlSegments;
if($segments){
$page = $basePage;
foreach($segments as $segment){
	$children = $page->children;
	foreach ($children as $child){
		$found = false;
		if($segment==toSlug($child->title)){
			$page = $child;
			$found = true;
			break;
		}
	}
	if (!$found) throw new Wire404Exception();
}
}
else {
$page = $basePage;
}
include("./{$page->template}.php");

This is all it does:

  • Changes the language to the name of this page (page must have same name as correspondent language)
  • For each url segment, it converts the title to a slug format, and looks for the children with the same name.
  • On the last segment it changes the $page object to the correspondent page object.
  • If no page is found, throws a 404.
  • Includes the template from the original page.

Maybe you could combine this with Ryan's solution for the URLs

  • Like 1
Link to comment
Share on other sites

Diogo, this is an interesting approach, thank you! I would extend it to use the toSlug function for the title only for the case if there is no individual name for the other language. And I would automatically create this field with a hook after page save. With that we can use the options of the page name module, hopefully.

Thanks again, I will try that!

Link to comment
Share on other sites

You're right. It's actually because it's also not in the docu.

It's true, I gave it a try and it worked:)

It returns an array with all the segments like this: ( [1] => segment1 [2] => segment2 )

Link to comment
Share on other sites

@diogo Thanks for sharing your code. I just packed it in a Module adding some features.

It extends the Page class, by hooking a new method mlUrl(bool includePageId).

I wanted to generate the localized url too. So instead of using names I use title to generate url.

That function accepts one boolean parameter to specify if to use the page id:

www.mydomain.com/it/my-italian-title/

www.mydomain.com/it/[page id]_my-italian-title

The id is useful if you can't be sure that the localized title is unique, or in case that there are a lot of contents and you don't want to search by title in a long loop, but seeking directly by id.

The generated url always contains the language code, for the default one too, that in this case is manually mapped to 'en';

This is the code (that could be improved, especially in the generation of the url):

<?php
class MultiLanguageURL extends WireData implements Module {
/**
 * getModuleInfo is a module required by all modules to tell ProcessWire about them
 *
 * @return array
 *
 */
public static function getModuleInfo() {
 return array(
  // The module'ss title, typically a little more descriptive than the class name
  'title' => 'MultiLanguageURL',
  // version: major, minor, revision, i.e. 100 = 1.0.0
  'version' => 100,
  // summary is brief description of what this module is
  'summary' => 'Multi language url',

  // Optional URL to more information about the module
  'href' => '',
  // singular=true: indicates that only one instance of the module is allowed.
  // This is usually what you want for modules that attach hooks.
  'singular' => true,
  // autoload=true: indicates the module should be started with ProcessWire.
  // This is necessary for any modules that attach runtime hooks, otherwise those
  // hooks won't get attached unless some other code calls the module on it's own.
  // Note that autoload modules are almost always also 'singular' (seen above).
  'autoload' => true,
  );
}
/**
 * Initialize the module
 *
 * ProcessWire calls this when the module is loaded. For 'autoload' modules, this will be called
 * when ProcessWire's API is ready. As a result, this is a good place to attach hooks.
 *
 */
public function init() {
 $this->addHook('Page::mlUrl', $this, 'mlUrl');
}

private function mlPath($page) {
 if($page->id === 1) return '/';
 $path = '';
 $parents = $page->parents();
 foreach($parents as $parent) if($parent->id > 1) $path .= "/".$this->toSlug($parent->title);
 return $path . '/' . $this->toSlug($page->title) . '/';
}

public function mlUrl($event) {
 $page = $event->object;
 $includeId = $event->arguments(0);
 $url = $this->mlPath($page);
 if ($includeId) {
  $segments = explode('/', $url);
  $index = count($segments)-2;
  $segments[$index] = $page->id.'_'.$segments[$index];
  $url = implode('/', $segments);
 }
 $lang = $this->user->language->name;
 if (!$lang || $lang=='default') $lang = 'en';
 $event->return = '/'.$lang.$url;
}


private function toSlug($str) {
 $str = strtolower(trim($str));
 $str = preg_replace('/[^a-z0-9-]/', '-', $str);
 $str = preg_replace('/-+/', "-", $str);
 return $str;
}

public function parseUrl() {
 $page = $this->fuel('page');
 $user = $this->fuel('user');
 $languages = $this->fuel('languages');
 $pages = $this->fuel('pages');
 $input = $this->fuel('input');

 $lang = $page->name;
 if ($lang == 'en') $lang='default';
 $user->language = $languages->get($lang);
 $basePage = $pages->get("/");
 $segments = $input->urlSegments;
 if($segments){
  $page = $basePage;
  foreach($segments as $segment){
// search of page id inside the segment
$parts = explode('_', $segment);
$pageid=null;
if (count($parts)>1 && is_numeric($parts[0])) {
 $pageid=$parts[0];
 $page = $pages->get($pageid);
} else {
 $children = $page->children;
 foreach ($children as $child){
  $found = false;
  if($segment==$this->toSlug($child->title)){
   $page = $child;
   $found = true;
   break;
  }
 }
}
if (!$found) throw new Wire404Exception();
  }
 } else {
  $page = $basePage;
 }
 $this->fuel->set('page', $page);
 return $page;
}
}

This is the code to use the module in your template:


<?php
$page = $modules->get('MultiLanguageURL')->parseUrl();
include("./{$page->template}.php");
?>

and to generate the localized url:

// to include the page id
$page->mlUrl(true);
// to generate without id
$page->mlUrl(false);
  • Like 5
Link to comment
Share on other sites

Wow, what a first post!

I can't test it now, but it looks great! Although I don't understand a big part of it... i will get there ;)

I just don't like the hard coded language

if ($lang == 'en') $lang='default';

Would be more elegant to have a setting on the module for this

static public function getModuleConfigInputfields(array $data) {

$data = array_merge(self::$defaults, $data);

$fields = new InputfieldWrapper();
$modules = Wire::getFuel("modules");

$field = $modules->get("InputfieldText");
$field->attr('name', 'defaultLang');
$field->attr('size', 10);
$field->attr('value', $data['en']);
$field->label = "Default language";
$field->description = "What is the url code you want to use for your defaul language?";
$field->notes = "Example: en, pt, gr or it";
$fields->append($field);

return $fields;
}
protected static $defaults = array(
'defaultLang' => 'en'
);
$options = self::$defaults;
if ($lang == $options['defaultLang']) $lang='default';
Link to comment
Share on other sites

Thanks guys for your appreciation :)

Would be more elegant to have a setting on the module for this

Yes, definitively is the best solution. I just started with ProcessWire and I didn't think about that possibility.

I'll try to implement it and optimize/document the code and I'll post it here again.

But my question is: is there a public repository of modules where to share it? I've seen a list in the documentation, but I suppose that is managed directly from Ryan or his colleagues.

Link to comment
Share on other sites

But my question is: is there a public repositories of modules where to share it?

Not yet, but I suggest that you start a new thread specifically for this module in the Modules section of the forum.

Since Ryan liked your post we can be confident that it will get listed once it's finished ;)

edit: on the modules section, maybe you could attach the module file to the post to make it easier to install

Link to comment
Share on other sites

Welcome mcmorry, and thanks for making this module.

The following is maybe more directed to Ryan, but everyone it welcome to think about something. :D

What would it mean to implement this feature in the core? Would it be a lot of work to change the url mapping to support language name for a page? (edit: or even possible or making sense). I mean since you already have implemented the multilang fields, and given that there's a page title multilang field, I find it now strange that such a module is required to implement language urls which is a "must have" feature, regarding SEO, for a good multilanguage site.

Sorry to bring this up here a little late. I just am thinking about this subject a very long time and not only since PW, but haven't really payed attention to the multilang field and title and that something seems missing as the "last" brick for a on-one-page multillanguage solution (with speaking urls) out of the box. I'm ever since using separate language trees to build sites and it works very well and flexible, consistent, fast, not so polluted page edit mask, etc. so I'm more the one heading towards the concept of Olivers language module with synced, linked multitrees. But this is a different subject and maybe discussed in the other room. :)

Link to comment
Share on other sites

I think it's good to have this sort of functionality coming from a module. After all, all of ProcessWire's language support comes from modules that aren't installed by default. Multi-tree is the recommended way to build a front-end multi language site. The only reason multi language fields exist is because we needed a way to support in the admin while using the same page. It was just bonus that they can be used on the front end too. And perhaps they are revealing themselves to be useful enough on the front-end to warrant taking it further in the future. But multi language fields are very far from being a full solution, as only one field type of dozens supports multi language (FieldtypeTextLanguage, which is inclusive of FieldtypePageTitleLanguage and FieldtypeTextareaLanguage). I also don't want to start modifying and adding overhead to the core for features that aren't needed by everyone. This is what modules were designed for. So even if we do future expansion of multi language fields, I still think this is the role of modules. It's great to see what Mcmorry has done with this module, a lot of good thinking and ideas. If more and more people want to handle languages with fields rather than pages, I'm sure that the options will continue to grow.

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...