Jump to content

Creating a module that auto-create pages when new pages are added


Recommended Posts

Hi,

I've been creating a site that has multiple languages but crucially does not use Processwire's awesome inbuilt multi-language support (some background here).

My site tree looks something like this...

Home

- Articles (English)

-- Article title

-- Article title

-- Article title

- French

-- Articles

--- Article title

--- Article title

--- Article title

- German

-- Articles

--- Article title

--- Article title

--- Article title

etc

Now the client want the page slugs to be the same on all languages, so for example an article will have a url like

  • domain.com/articles/name-of-article/
  • domain.com/fr/articles/name-of-article/
  • domain.com/de/articles/name-of-article/

etc.

The problem...

To make life easier for the translators they want the translated articles to auto-generate that page slug. Processwire can't really do that when a translator adds it as it doesn't know which article it is a translation of.

Once on the edit screen I have a PageFieldType so the translator has to explicitly select the English version that their post is a translation of. This makes it easy for me in the templates to share content that doesn't need translation such as main image etc.

So...

I either need a way to extract the slug from the English version and use it for translated versions auotmatically (once the PageFieldType connection is made) overiding any slug that the translators have used.

or even better....

I thought a module that automatically creates an unpublished page under /articles/ in all 12 languages with the same page slug when a new English article is added under /articles/.

Would this be possible with Processwire? I'm not sure where to start (I'm very much dipping my toes in modules but have used Processwire extensively for projects previously).

Any help/guidance would be massively appreciated, thanks!

Link to comment
Share on other sites

You could create a module, that inserts a hook before page save for article pages, that creates such pages programmatically.

They say, creating modules is easy.

Thanks Ivan for pointing me in the right direction - I'll see how I get on!

Just out of interest do you think the other route is possible? ie. once an editor on the translated version has selected the English version from the pagefieldtype, using the English versions page slug for the translated version in the templates?

Link to comment
Share on other sites

<?php
class DuplicateArticles extends WireData implements Module {

	public static function getModuleInfo() {
		return array(

			'title' => 'Duplicate Articles When Created In English To All Translations',
			'version' => 1,
			'summary' => 'Module to create duplicate of English (master) articles as pages in every translated language.',
			'singular' => true,
			'autoload' => true,
			);
	}

	public function init() {
		$this->pages->addHookAfter('added', $this, 'duplicate');
	}

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

// Only duplicate pages under /articles/
	if ($page->parent_id == 1023) {

		$a = new Page();
		$a->template = 'article_french';
		$a->parent = wire('pages')->get('/fr/articles/');
		$a->name = $page->name;
		$a->title = "{$page->title} (awaiting translation)";
		$a->status = Page::statusUnpublished;
		$a->save();

		$b = new Page();
		$b->template = 'article_spanish';
		$b->parent = wire('pages')->get('/es/articles/');
		$b->name = $page->name;
		$b->title = "{$page->title} (awaiting translation)";
		$b->status = Page::statusUnpublished;
		$b->save();

		$c = new Page();
		$c->template = 'article_german';
		$c->parent = wire('pages')->get('/de/articles/');
		$c->name = $page->name;
		$c->title = "{$page->title} (awaiting translation)";
		$c->status = Page::statusUnpublished;
		$c->save();

		$d = new Page();
		$d->template = 'article_russian';
		$d->parent = wire('pages')->get('/ru/articles/');
		$d->name = $page->name;
		$d->title = "{$page->title} (awaiting translation)";
		$d->status = Page::statusUnpublished;
		$d->save();

		$e = new Page();
		$e->template = 'article_korean';
		$e->parent = wire('pages')->get('/ko/articles/');
		$e->name = $page->name;
		$e->title = "{$page->title} (awaiting translation)";
		$e->status = Page::statusUnpublished;
		$e->save();

		}
	}
}

Ok my first module... and it works! Woah. I'm only showing 5x languages above. Eventually there will be 12. Is 12x actions on a page save okay?

So now I need to figure out...

- More elegant way of creating each new page - there's a lot of duplicated code

- A way of deleting the translated pages if the English is deleted

- A way of pre-filling a pagefieldtype with the English page - this pagefieldtype is to to explicitly set the related English version to the translated version. I tried adding...

$a->id = $page->related_page_language->id;

But that doesn't do anything. Anyone know how I might achieve this?

  • Like 1
Link to comment
Share on other sites

So now I need to figure out...

- More elegant way of creating each new page - there's a lot of duplicated code

Try $pages->clone($page[, $parent]) and just change what needs to be changed.

- A way of deleting the translated pages if the English is deleted

Add a pagefield to the template of your articles, where you store all the translation pages (if you don't already have that). Hook Pages::delete and if it's an english article delete all the others, too. You can even hide this field from the backend, so admins cannot tamper with it.

- A way of pre-filling a pagefieldtype with the English page - this pagefieldtype is to to explicitly set the related English version to the translated version

I'm not sure if I understand correctly what you want to archive, but setting the id of page $a seems to be wrong.

// Add the english page to the field "related_page_language" on page $a.

// Single page
$a->related_page_language = $page;

// Multiple pages
$a->related_page_language->add($page->id);
Link to comment
Share on other sites

Try $pages->clone($page[, $parent]) and just change what needs to be changed.

Slightly lost on this one, sorry! How would this change what I have?

Add a pagefield to the template of your articles, where you store all the translation pages (if you don't already have that). Hook Pages::delete and if it's an english article delete all the others, too. You can even hide this field from the backend, so admins cannot tamper with it.

I think I get the logic here - however wouldn't it be that if there *isn't* an English version then it deletes all others? Translated articles therefore can't exist without a 'master' English original.

// Add the english page to the field "related_page_language" on page $a.

// Single page
$a->related_page_language = $page;

// Multiple pages
$a->related_page_language->add($page->id);

Just to clarify this is for a page field on the translated articles that connects them back to the original English article. I've used this in the templates to to get shared content such as the main image, rather than having to duplicate this content for each translation.

I was tearing my hair out trying to get this to work and then realised I hadn't checked 'allow unpublished pages' in the page field settings. Doh. This works perfectly as you have above now.

Thank you!

Link to comment
Share on other sites

$a = new Page();
$a->template = 'article_french';
$a->parent = wire('pages')->get('/fr/articles/');
$a->name = $page->name;
$a->title = "{$page->title} (awaiting translation)";
$a->status = Page::statusUnpublished;
$a->save();

$b = wire('pages')->clone($a, wire('pages')->get('/es/articles/'));
$b->template = 'article_spanish';
$b->save();

$c = wire('pages')->clone(…);
…

I think I get the logic here - however wouldn't it be that if there *isn't* an English version then it deletes all others? Translated articles therefore can't exist without a 'master' English original.

Depending on how you need it. Hooking Pages::delete runs only if a page (the english one) is deleted. So if there's no english version then potential translations won't be removed. If you want to check for orphans in the translations more often you could also hook Pages::saveReady, which runs every time a page is ready to be saved to the database.

Link to comment
Share on other sites

$a = new Page();
$a->template = 'article_french';
$a->parent = wire('pages')->get('/fr/articles/');
$a->name = $page->name;
$a->title = "{$page->title} (awaiting translation)";
$a->status = Page::statusUnpublished;
$a->save();

$b = wire('pages')->clone($a, wire('pages')->get('/es/articles/'));
$b->template = 'article_spanish';
$b->save();

$c = wire('pages')->clone(…);
…

Depending on how you need it. Hooking Pages::delete runs only if a page (the english one) is deleted. So if there's no english version then potential translations won't be removed. If you want to check for orphans in the translations more often you could also hook Pages::saveReady, which runs every time a page is ready to be saved to the database.

Thanks so much for your help, using clone now makes sense (ie. you have to do the first one to then clone it, I was slipping up here). Got it working perfectly.

You're right it'll be best to only delete translations if the English version is deleted. Still a little way off on exactly the best way to achieve this...

So as I'm understanding it, the steps are:

  1. An english article has a hidden pagefield with all its translations in
  2. If an english article is deleted, all the translation pages listed in that field are deleted

Now I'm guessing that when the duplicates are made, I can also populate the pagefield on the English article with all the duplicate translations in step (1)? I'm not quite sure where to start with this within my current module. 

Link to comment
Share on other sites

<?php
class DuplicateArticles extends WireData implements Module {

	public static function getModuleInfo() {
		return array(

			'title' => 'Duplicate Articles When Created In English To All Translations',
			'version' => 1,
			'summary' => 'Module to create duplicate of English (master) articles as pages in every translated language.',
			'singular' => true,
			'autoload' => true,
			);
	}

	public function init() {
		$this->pages->addHookAfter('added', $this, 'duplicate');
		// This hook occurs right after saving the trash status
		$this->pages->addHook('trashed', $this, 'trashDuplicates');
		// This hook occurs right before final deletion
		$this->pages->addHook('deleteReady', $this, 'deleteDuplicates');
	}

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

		// Only duplicate pages under /articles/
		if ($page->parent_id == 1023) {

			$translations = new PageArray();

			$a = new Page();
			$a->template = 'article_french';
			$a->parent = wire('pages')->get('/fr/articles/');
			$a->name = $page->name;
			$a->title = "{$page->title} (awaiting translation)";
			$a->status = Page::statusUnpublished;
			$a->save();
			$translations->add($a);

			$b = new Page();
			$b->template = 'article_spanish';
			$b->parent = wire('pages')->get('/es/articles/');
			$b->name = $page->name;
			$b->title = "{$page->title} (awaiting translation)";
			$b->status = Page::statusUnpublished;
			$b->save();
			$translations->add($b);

			…

			$page->translationPages->import($translations);
			$page->save("translationPages");

		}
	}

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

		if($page->parent_id == 1023){
			foreach($page->translationPages as $translation){
				$pages->trash($translation);
			}
		}
	}

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

		// It may be trashed, where the parent is not 1023
		if($page->is("template=article")){
			foreach($page->translationPages as $translation){
				$pages->delete($translation, true);
			}
		}
	}
}

Not tested. Maybe it works out of the box, but at least it'll get you started.

  • Like 2
Link to comment
Share on other sites

<?php
class DuplicateArticles extends WireData implements Module {

	public static function getModuleInfo() {
		return array(

			'title' => 'Duplicate Articles When Created In English To All Translations',
			'version' => 1,
			'summary' => 'Module to create duplicate of English (master) articles as pages in every translated language.',
			'singular' => true,
			'autoload' => true,
			);
	}

	public function init() {
		$this->pages->addHookAfter('added', $this, 'duplicate');
		// This hook occurs right after saving the trash status
		$this->pages->addHook('trashed', $this, 'trashDuplicates');
		// This hook occurs right before final deletion
		$this->pages->addHook('deleteReady', $this, 'deleteDuplicates');
	}

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

		// Only duplicate pages under /articles/
		if ($page->parent_id == 1023) {

			$translations = new PageArray();

			$a = new Page();
			$a->template = 'article_french';
			$a->parent = wire('pages')->get('/fr/articles/');
			$a->name = $page->name;
			$a->title = "{$page->title} (awaiting translation)";
			$a->status = Page::statusUnpublished;
			$a->save();
			$translations->add($a);

			$b = new Page();
			$b->template = 'article_spanish';
			$b->parent = wire('pages')->get('/es/articles/');
			$b->name = $page->name;
			$b->title = "{$page->title} (awaiting translation)";
			$b->status = Page::statusUnpublished;
			$b->save();
			$translations->add($b);

			…

			$page->translationPages->import($translations);
			$page->save("translationPages");

		}
	}

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

		if($page->parent_id == 1023){
			foreach($page->translationPages as $translation){
				$pages->trash($translation);
			}
		}
	}

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

		// It may be trashed, where the parent is not 1023
		if($page->is("template=article")){
			foreach($page->translationPages as $translation){
				$pages->delete($translation, true);
			}
		}
	}
}

Not tested. Maybe it works out of the box, but at least it'll get you started.

Ah ha! The pagefield is now being successfully filled with all the translated versions when the English article is created.

However the deleting functionality doesn't seem to do anything. I thought could this be to do with the fact that I'm deleting unpublished pages?

Link to comment
Share on other sites

<?php
class DuplicateArticles extends WireData implements Module {

	public static function getModuleInfo() {
		return array(

			'title' => 'Duplicate Articles When Created In English To All Translations',
			'version' => 1,
			'summary' => 'Module to create duplicate of English (master) articles as pages in every translated language.',
			'singular' => true,
			'autoload' => true,
			);
	}

	public function init() {
		$this->pages->addHookAfter('added', $this, 'duplicate');
		// This hook occurs right after saving the trash status
		$this->pages->addHook('trashed', $this, 'trashDuplicates');
		// This hook occurs right before final deletion
		$this->pages->addHook('deleteReady', $this, 'deleteDuplicates');
	}

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

		// Only duplicate pages under /articles/
		if ($page->parent_id == 1023) {

			$translations = new PageArray();

			$a = new Page();
			$a->template = 'article_french';
			$a->parent = wire('pages')->get('/fr/articles/');
			$a->name = $page->name;
			$a->title = "{$page->title} (awaiting translation)";
			$a->related_page_language = $page;
			$a->status = Page::statusUnpublished;
			$a->save();
			$translations->add($a);

			$b = new Page();
			$b->template = 'article_spanish';
			$b->parent = wire('pages')->get('/es/articles/');
			$b->name = $page->name;
			$b->title = "{$page->title} (awaiting translation)";
			$b->related_page_language = $page;
			$b->status = Page::statusUnpublished;
			$b->save();
			$translations->add($b);

			$c = new Page();
			$c->template = 'article_german';
			$c->parent = wire('pages')->get('/de/articles/');
			$c->name = $page->name;
			$c->title = "{$page->title} (awaiting translation)";
			$c->related_page_language = $page;
			$c->status = Page::statusUnpublished;
			$c->save();
			$translations->add($c);

			$d = new Page();
			$d->template = 'article_russian';
			$d->parent = wire('pages')->get('/ru/articles/');
			$d->name = $page->name;
			$d->title = "{$page->title} (awaiting translation)";
			$d->related_page_language = $page;
			$d->status = Page::statusUnpublished;
			$d->save();
			$translations->add($d);

			$e = new Page();
			$e->template = 'article_korean';
			$e->parent = wire('pages')->get('/ko/articles/');
			$e->name = $page->name;
			$e->title = "{$page->title} (awaiting translation)";
			$e->related_page_language = $page;
			$e->status = Page::statusUnpublished;
			$e->save();
			$translations->add($e);

			$page->translated_pages->import($translations);
			$page->save("translated_pages");
		}
	}

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

		if($page->parent_id == 1023){
			foreach($page->translated_pages as $translation){
				$pages->trash($translation);
			}
		}
	}

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

		// It may be trashed, where the parent is not 1023
		if($page->is("template=article_english")){
			foreach($page->translated_pages as $translation){
				$pages->delete($translation, true);
			}
		}
	}
}

This is the working module (with deleting functionality not working).

I also noticed it's breaking the emptying of trash. I thought this line could be an issue...

if($page->is("template=article_english")){

Should this be the templates for the translations not the english version? Can't quite work out which it should be referencing here.

Edited by alexcapes
Link to comment
Share on other sites

It's the right one. You want to delete the translations when the english article is deleted, which in this case it $page. It's in all three of your methods the same that you're initializing additional changes if the english article happens to change/be added. But sadly I can't see why it's not working. It not like it's much code which could have errors.

Link to comment
Share on other sites

It's the right one. You want to delete the translations when the english article is deleted, which in this case it $page. It's in all three of your methods the same that you're initializing additional changes if the english article happens to change/be added. But sadly I can't see why it's not working. It not like it's much code which could have errors.

When I attempt to empty the trash this is the error I get...

Error: Call to a member function delete() on a non-object (line 102 of /DuplicateArticles.module) 

This only comes up when trying to empty the trash containing a page using 'article_english' as the template.

Does that shed any light?

Link to comment
Share on other sites

Yeah, use $this->pages...

Still no joy...

Notice: Undefined variable: pages in /DuplicateArticles.module on line 102

Fatal error: Call to a member function delete() on a non-object in /DuplicateArticles.module on line 102

... on emptying trash with 'article_english' pages in.

Link to comment
Share on other sites

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

		if($page->parent_id == 1023){
			foreach($page->translated_pages as $translation){
				$this->pages->trash($translation);
			}
		}
	}

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

		// It may be trashed, where the parent is not 1023
		if($page->is("template=article_english")){
			foreach($page->translated_pages as $translation){
				$this->pages->delete($translation, true);
			}
		}
	}

This code shouldn't error like your error message, as pages is no longer the variable.

  • Like 1
Link to comment
Share on other sites

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

		if($page->parent_id == 1023){
			foreach($page->translated_pages as $translation){
				$this->pages->trash($translation);
			}
		}
	}

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

		// It may be trashed, where the parent is not 1023
		if($page->is("template=article_english")){
			foreach($page->translated_pages as $translation){
				$this->pages->delete($translation, true);
			}
		}
	}

This code shouldn't error like your error message, as pages is no longer the variable.

Ok my bad, I was putting $this->$page... doh. So it's working!

However only the deleting was working, but after some trial and error I found out this...

if($page->parent_id == 1023){

..wasn't returning anything. So I tried this instead...

if($page->is("template=article_english")){

... and it seems to all work!

Thank you LostKobrakai for your knowledge and patience.

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

×
×
  • Create New...