Jump to content

Module: Languages (multi language content management)


Oliver
 Share

Recommended Posts

Don’t use with PW2.2 because of potential conflicts with its built-in multi-language support.

Module will be replaced by a «context» manager module allowing you to define language specific page trees (among other possibilities) based upon PW2.2’s own language management.

Download Module ProcessLanguages, Version 0.08, more alpha than beta, from GitHub:

https://github.com/L...rocessLanguages

# 2011-11-19 0.08. Language specific error404 pages implemented.

# 2011-11-10 Test version with basic features (0.07). Added template variable to enable switching between translations.

# 2011-11-10 Test version with basic features (0.05). Uninstall still gets kind of manual. ;) Mapping doesn’t work properly, when pages from outside are moved into a language’s page tree.


And that’s how it all started:

Hey, I’m working on an module to implement multiple language support in ProcessWire.

Until now the module provides an admin panel to set up additional languages. Every language is represented in the site’s page hierarchy by an own home page automatically generated as a child of the site’s root. The language’s home page id is associated with the language entry in the admin panel. If the site’s root is called, a hooked function automatically redirects to the home page of the language defined as default.

The module is inspired by the concept of modx module Babel by Jacob Class.

The next steps on my list are:

  • Setting up a db table associating the different language versions of a page with each other.
  • (If possible: ) Adding an additional tab to the Page Edit top menu (Content, Children, etc.) providing an overview of the other language versions of the edited page…
    Question: Is there a possibility to hook into an event allowing me to change the page edit top menu and add a "language" tab to the edit form? Any idea?
  • … and allowing to create language clones of a page by copying it to other languages’ page structure.

Would be great if there were some hints or suggestions to make it work and maybe more useful in the end!

Thanks in advance.

Link to comment
Share on other sites

The cloning won’t happen automatically. If it works out the way I want it to, you’ll be able to mark the languages in the page edit languages tab you want a clone to be created for. Maybe there should be an option to mirror the default language’s page structure completely on creation of an additional language, too?

---

Jbroussia, just found your post: http://processwire.com/talk/index.php/topic,391.0.html

Funny, you were thinking of the same approach just two weeks before me!

---

As you guys already talked about in the other thread, cloning pages isn’t that easy.

I decided to deal with cloning a languages page tree completely on creation of a new language–as long as I have no idea how to manipulate the page edit admin page. But because of the complexity of the page-fields-structure, I’ve no idea of all the things that have to be duplicated with and for each page to get a kind of clone–in view of fields, values, roles etc.

Would be great to get a hint here, too.  ???

[Edited by Adam] Glued three of your posts together. If you want to enhance/edit your message (add another idea(s), etc.), please use 'Edit' function, instead of double (triple, ...) posting. Thank you!

Link to comment
Share on other sites

Since you guys need this sooner rather than later, I'll put it higher up in the priority list. Cloning a page will be relatively easy. Haven't tried it, but I think this may work:

<?php
$cloned = clone $page; 
$cloned->setIsNew(true); 
$cloned->id = 0; 
$cloned->name = $cloned->name . "-clone";
$cloned->save();

Assuming that much works, what I can say for certain would not work is file fields. This is where the process would become more complex than this, though shouldn't be bad. I'm adding this to the to-do queue on GitHub.

Adding another tab (like a 'language' tab) to the PageEdit screen is going to most likely involve hooking into the ProcessPageEdit::buildForm function and appending another InputfieldWrapper to the form it returns. Probably best for me to setup an example for this–let me know when you are at the point where you want it and I'll get it going.

Thanks,

Ryan

Link to comment
Share on other sites

Oh in my case there is not need to hurry, the multi-language site I'm planning to do will only have a few pages, for now I can still do manual copies.

Does this clone function clone pages *and* their children ?

And what is "setIsNew" for ?

Link to comment
Share on other sites

That code snippet above wouldn't clone a tree, just a single page. When this is implemented in the API, we'd let it be optional as to whether to clone the tree (i.e. be recursive). But we'd definitely provide that option, just like the delete() function provides that option (though requires it if the page has children).

That setIsNew function isn't part of the public API, but it would be necessary in this case to tell PW that this is a new page it should create when it comes time to save. Otherwise the cloned copy would still remember that it was originally loaded from the DB. I can't think of another situation where you would ever need to use the setIsNew function, so that probably won't ever make it into the public API.

Link to comment
Share on other sites

Thanks for the input, Ryan. I already experimented with cloning a page and at least got a page transfer from one language tree to another. I guess I’ve to go through the fields one by one and create a copy by setting an new field of the same type and duplicating the data/values/files associated with it. Do I also have to clone any db data–if there is–of role or permission objects related to the page?

As soon as the cloning process works, I would focus on the implementation of the language management in the page edit form. So I let you know when I got there!

Link to comment
Share on other sites

I'm assuming you are using PW 2.1 for all of this. But let me know if not. You are working with stuff that is somewhat different between PW 2.0 and 2.1, and I would definitely recommend sticking with 2.1 if you are creating new modules since we're just about to the point of making it the current stable version.  

You shouldn't have to clone any of the fields (or database entries), PW will do that for you. I just tested the example here and it did work for me (cloning the page and the fields). Though I should note I did have to add $cloned->setOutputFormatting(false); before I saved it. That would only be necessary if you put the code snippet in a template file, as I did.

The only field I can think of that it would not currently work for is a file or image field... but I'll work on that.

You don't have to clone any role/permission stuff because those are assigned at the template, not the page. So your clone will simply inherit the same roles from the template as the original did.

Do you need the recursive/children cloning like jbroussia mentioned? If so let me know because I will want to get that new $pages->clone function in place this week so that you don't have to wait for this.

Link to comment
Share on other sites

Recursive cloning as an option would be great!

If you post me the example code for extending the page edit form, I could work on the language versions interface while you are working on your "clone" update.

And to answer your question: Yes, I’m using 2.1.

Link to comment
Share on other sites

If I understand correctly, cloning file or image fields is different because those are stored in the file system instead of the DB !? What about the other fields, do you clone them by creating new copies of the content in the DB, or do you "link" new fields to the original fields ID in the DB ?

Link to comment
Share on other sites

@jbroussia: PW is writing all new fields when you do $page->save(). Since we set it's ID as 0 and called $page->setIsNew(true), we tricked ProcessWire into thinking it was a brand new page that it had never saved before. So from that point it is creating all new fields when you save it.

Also, when we did "$cloned = clone $page;" it automatically cloned all the fields in memory too, so $page and $clone aren't sharing the same memory even for that moment. This occurs because PW implements PHP5's __clone() method and specifically sets up new fields at at that time. Admittedly, this is one of the first opportunities I've had to actually use it.

You are right that file fields are different because they have corresponding files on the disk. PW needs to know where to place those files, and there's no way for it to know until the page has actually been saved (since the directories are based on the page's ID). So this is one of the details we would have to take care of with a $pages->clone function by saving/creating the page, then copying the files, then saving again.  

Link to comment
Share on other sites

Here is an example module for adding tabs to a page. I tried to cover a few things you might want to do after you've added the tab too, just as examples. If you want to download the module file (rather than copying/pasting), it is attached at the end of this message.

<?php

class PageTabExample extends WireData implements Module {

   /**
    * Provide information about this module to ProcessWire
    *
    */
   public static function getModuleInfo() {
       return array(
           'title' => 'Page Tab Example',
           'summary' => 'Example of adding tabs to the page editor',
           'version' => 001,
           'permanent' => false,
           'autoload' => true,
           'singular' => true,
           );
   }

   /**
    * Add the hook
    *
    */
   public function init() {
       $this->addHookAfter("ProcessPageEdit::buildForm", $this, 'buildForm');
   }

   /**
    * Hook called after PageEdit::buildForm is called
    *
    */
   public function buildForm(HookEvent $event) {
       $form = $event->return;
       $this->example1($form);
       $this->example2($form);
       $event->return = $form;
   }

   /**
    * Add a simple tab to the page edit form
    *
    * Note that would technically add the tab link after the 'view' link on the form
    * See example2 to see how we get the view link at the end (assuming that's where you want it)
    *
    */
   protected function example1($form) {

       // create the new tab
       $tab = new InputfieldWrapper();
       $tab->attr('id', 'tabExample1');
       $tab->attr('title', 'Example 1');

       // create a general markup field as an example
       $field = $this->modules->get("InputfieldMarkup");
       $field->attr('name', 'fieldExample1');
       $field->label = "Example 1 field";
       $field->attr('value', "<p>This can contain any markup you want</p>");

       // add the field to the tab
       $tab->add($field);

       // add the tab to the form
       $form->add($tab);
   }

   /**
    * Add a tab before the 'view' link
    *
    * Also demonstrates setting or retrieving a value submitted to a field in the tab.
    *
    */
   protected function example2($form) {

       // first we have to find and remove the view link, temporarily
       $view = $form->children('id=ProcessPageEditView')->first();
       $form->remove($view);

       // create the tab
       $tab = new InputfieldWrapper();
       $tab->attr('id', 'tabExample2');
       $tab->attr('title', 'Example 2');

       // now add our new tab and field
       $field = $this->modules->get("InputfieldTextarea");
       $field->attr('name', 'fieldExample2');
       $field->label = 'Example 2 field';
       $field->description =
           "Enter some text. The text you enter won't get saved with the page, " .
           "so you'd have to do something with it in the same hook that displays this.";

       // since this isn't technically part of the page's fields, we have to
       // handle any input submitting to the field if we want it.
       if($this->input->post->fieldExample2 !== null) {
           $field->processInput($this->input->post);
           // we'll store in the temporary session var just an example.
           // using session just for example, you likely would save your value elsewhere.
           $this->session->fieldExample2 = $field->attr('value');
       } else {
           // retrieve the value from the session var.
           $field->attr('value', $this->session->fieldExample2);
       }

       // add the field to the tab
       $tab->add($field);

       // add the tab to the form
       $form->add($tab);

       // now add that view link back to the form at the end
       $form->add($view);
   }
}

PageTabExample.module

Link to comment
Share on other sites

Thanks a lot for the example, Ryan. Works pretty fine. Got the language tab working and just have to finalize the listing of the associated versions and the processing of the form/mapping in the database. Will look into that tomorrow evening.

If we get the cloning done, maybe I can provide a test version of the module to the end of the week.

Link to comment
Share on other sites

Hey guys, I've added the $pages->clone() function to the API and you'll see it in the latest commit (or the one before it). I've tested on a few pages with files and children and so far so good. There's a lot that it has to do, so this is not something I'd use to clone a 1000 page tree, but it should be fine recursively cloning most stuff.

I'd like to do more testing but have run out of time for today, and opted to go ahead and push it to the source tonight so that I could get it to you sooner rather than later. I think it's pretty solid, but just to be safe, I recommend considering that function unstable until we've run it through a lot more scenarios.

Here's how to use it:

<?php

// clone a page or tree of pages
$copy = $pages->clone($page); 

// clone a page or tree of pages, and give it a new parent
$copy = $pages->clone($page, $newParent); 

The $copy that is returned is already in the database, ready to go, and does not need to be saved again. Please let me know if you run into any issues with it. Don't use in a production environment until we've had a few more people test it.

Thanks,

Ryan

Link to comment
Share on other sites

Thanks Ryan, you are the best!

I updated the core files with your changes and implemented the cloning. Works fine so far.

Some important steps to make for me, yet:

  • Setting up hooks dealing with manually created pages in language trees, deleting pages from language trees, moving pages in, out of or into trash from language trees to keep the pagemap consistent. The pagemap associates all the different language version of a page with each other.
  • Creating new translation from the "Languages" tab of the page edit dialog. Offering "by cloning current page" and "by creating new empty page". Maybe there also has to be the possibility to map the currently edited page manually with an existing one?
  • Providing an var $languages to make a list of all languages (for language links), the current page’s translations in the different languages as well as the current active language accessible in templates.

As soon as I got the languages tab processing part done, I’ll post a download link here for you guys to have a look. Would love to get some feedback on this.

Link to comment
Share on other sites

Sounds great, this can be very helpful for creating multilanguage sites. Many thank for working on that. I surely would like to help test when a first release candidate is ready.

I'm working here in Switzerland. Having 4 native languages, most projects require multilanguage sites.

Link to comment
Share on other sites

Soma, so we are kind of neighbours. I’m living and working in Basel.

Can anyone give me a hint regarding the process hooks? How do I access the created/deleted/trashed page’s id (or page object) in my method dealing with the passed event object?

Link to comment
Share on other sites

Setting up hooks dealing with manually created pages in language trees, deleting pages from language trees, moving pages in, out of or into trash from language trees to keep the pagemap consistent. The pagemap associates all the different language version of a page with each other.

Sounds like a lot to keep track of. But I think you'll be able to do most (or all) of it by hooking into:

$pages->save()

$pages->delete()

$pages->trash()

$pages->restore()

If you only want to hook these things in the interactive PageEdit, as opposed to all of the API, then you'll want your hooks to check the value of wire('process') (or $this->process) and confirm that it is 'ProcessPageEdit'.

Let me know if you find you need other hooks that we don't already have. In PW, making a function hookable is as easy as prepending 3 underscores to the beginning of it. So I've not attempted to add all possible hooks in the system, instead taking the approach of adding new core hooks as needs arise.

Creating new translation from the "Languages" tab of the page edit dialog. Offering "by cloning current page" and "by creating new empty page". Maybe there also has to be the possibility to map the currently edited page manually with an existing one?

Sounds great! You've really thought this through. I like all that I hear.

Providing an var $languages to make a list of all languages (for language links), the current page’s translations in the different languages as well as the current active language accessible in templates.

In PW, you can make a new API var (available to all templates and modules) by calling:

Wire::setFuel('api_var_name', $your_api_var); 

As soon as I got the languages tab processing part done, I’ll post a download link here for you guys to have a look. Would love to get some feedback on this.

Thanks, looking forward to it!

Link to comment
Share on other sites

Can anyone give me a hint regarding the process hooks? How do I access the created/deleted/trashed page’s id (or page object) in my method dealing with the passed event object?

Here are examples of hooks you asked about:

<?php

public function init() {
    $this->pages->addHookBefore('save', $this, 'pagesBeforeSave'); 
    $this->pages->addHookAfter('save', $this, 'pagesAfterSave'); 
    $this->pages->addHookAfter('delete', $this, 'pagesAfterDelete'); 
    $this->pages->addHookAfter('trash', $this, 'pagesAfterTrash'); 
    $this->pages->addHookAfter('restore', $this, 'pagesAfterRestore'); 
}

public function pagesBeforeSave(HookEvent $event) {
    $page = $event->arguments[0]; 
    if(!$page->id) { 
        // set your own var name to check after page saved
        $page->this_is_new = true; 
        // do anything else you want here
    } else {
        // page is an existing page
    }
}

public function pagesAfterSave(HookEvent $event) {
    $page = $event->arguments[0]; 

    if($page->this_is_new) {
        // check the var you set before to determine if it's new 
        // page now has an ID, so do something with it if you want
    }

    if($page->parentPrevious) {
        // page was moved. $page->parent is new parent
        // and $page->parentPrevious is old parent
    }
}

public function pagesAfterDelete(HookEvent $event) {
    $page = $event->arguments[0]; 
    if($page->is(Page::statusDeleted)) {
        // page was deleted
    }
}

public function pagesAfterTrash(HookEvent $event) {
    $page = $event->arguments[0];
    // page was moved to the trash
}

public function pagesAfterRestore(HookEvent $event) {
    $page = $event->arguments[0];
    // page was moved out of the trash
}

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...