J3.x

Desarrollo de un Componente MVC/Usar la facilidad filtro de idioma

From Joomla! Documentation

< J3.x:Developing an MVC Component
This page is a translated version of the page J3.x:Developing an MVC Component/Using the language filter facility and the translation is 4% complete.

Outdated translations are marked like this.
Other languages:
العربية • ‎English • ‎español • ‎français • ‎Nederlands
Joomla! 
3.x
Tutorial
Desarrollo de un Componente MVC


Esta es una serie multi-artículos de tutoriales sobre cómo desarrollar un Componente Modelo-Vista-Controlador para Joomla! VersiónJoomla 3.x.

Comenzar con la Introducción, y navegar por los artículos de esta serie usando el botón de navegación en la parte inferior o en el cuadro de la derecha (los "Artículos de esta serie").



Este artículo es parte del tutorial Desarrollo de un Componente MVC para Joomla! 3.2. Te invitamos a leer las partes anteriores del tutorial antes de leer esto.

In this step we make our helloworld records multilingual by adding a language field to the database record and build the associated functionality to align with the Joomla principles of multilingual content.

Note that this step will not cover language associations (topic for a later tutorial step) nor any specific functionality for right-to-left languages.

In association with this step we'll build a more sophisticated routing regime than the previous step, and will look at some APIs around Joomla Categories.

Two videos accompanying this step are available, covering General Multilingual Aspects and Categories APIs and updated Custom Router.

Functionality

We add a language field to the helloworld record structure, and build the admin functionality to manage the maintenance of the language data. In addition we update the front-end form to allow the user to specify the language.

Throughout the site we'll ensure that helloworld links which are put onto the site are qualified with the appropriate language parameter, and when we're displaying helloworld data we'll ensure it's restricted to the appropriate language.

We'll change our Category view to display not only the helloworld records with that category, but also the subcategories of the category, so that a user can explore the category tree, and the helloworld records having each category. We'll make the form of the URLs so that it shows the subcategories (split below by formal / informal greetings) traversed to reach the helloworld record, for example

  • /en/messages/37:messages/38:formal/13:good-morning
  • /fr/salutations/40:salutations/42:familiers/17:ciao
  • /es/mensajes/43:mensajes/45:informales/12:hasta-luego

In each case we're showing the id:alias of the record.

We'll relax our condition that the alias be unique ("Ciao" is a greeting in several languages!) and instead have the combination of alias and helloworld category unique, as it is for Joomla Articles.

If we go via the Category List menu item and then search on the map using Ajax then we get URLs of the form above for the helloworld record. However, if instead we go initially via a Hello World menuitem, which is a menuitem pointing to an individual helloworld record, then when we search on the map we'll change the URLs so that they're on the /message URL segment instead, and the same for the other languages:

  • en/message/2:goodbye-world - (note "message" instead of "messages", etc)
  • fr/salutation/5:bonjour
  • es/mensaje/12:hasta-luego

where the last segment is helloworld record id + alias (id being needed to make the URL unique).

Approach

Configuration

To build this step it's essential that you configure your site as multilingual as described in this tutorial Setup a Multilingual Site, and configure at least 2 languages (eg English, French, Spanish).

Step 4 on Adding Multilingual Content isn't strictly necessary to set up a multilingual site, but it is strongly recommended, as setting top-level categories for each of the languages enables you to have the same alias for an article and its counterpart in another language. Also if you're introducing a new language on your site then it allows you to use a batch process for duplicating all of the articles in a current language into the new one, ready for the translation process.

We will follow this paradigm for our helloworld records, so configure the following helloworld categories (substituting your own choice of language if you prefer) and menu items. First of all though, delete the menuitems you already have which have aliases "messages" and "message", and ensure that you empty the trash. Then set up:

  • a top level helloworld category with title "messages" and alias "messages" and set to English language
  • a top level helloworld category with title "salutations" and alias "salutations" set to French
  • a top level helloworld category with title "mensajes" and alias "mensajes" set to Spanish

(The helloworld categories are already set up to support multiple languages because this is part of Joomla core functionality, even though the helloworld records themselves aren't). Now create menuitems on the language-specific menus, with each alias being the same as the top-level category of that language:

  • a menuitem on the "english" menu pointing to a Helloworld Category List view, with alias "messages" and language "English", and select the category "messages"
  • a menuitem on the "français" menu pointing to a Helloworld Category List view, with alias "salutations" and language "French", and select the category "salutations"
  • a menuitem on the "español" menu pointing to a Helloworld Category List view, with alias "mensajes" and language "Spanish", and select the category "mensajes"

(Joomla won't allow you to create 2 different menu items at the top level of 2 different menus if they've got the same alias, so we can't give the French menuitem an alias of "messages" as well).

And for the menu item on which we'll attach the results of our Ajax "Search here" call:

  • a hidden menuitem on the "english" menu pointing to a Helloworld item, with alias "message", language "English" and note "Ajax", and select any helloworld record
  • a hidden menuitem on the "français" menu pointing to a Helloworld item, with alias "salutation", language "French" and note "Ajax", and select any helloworld record
  • a hidden menuitem on the "español" menu pointing to a Helloworld item, with alias "mensaje", language "Spanish" and note "Ajax", and select any helloworld record

Database

We add a language field, and prepopulate this with '*', signifying the records related to All languages.

We'll drop the old unique index on alias, and replace it with a unique index on alias + category id.

Admin Management

We'll add a Joomla language field to our edit form to allow administrators to define the language. We'll display the language on our helloworlds layout, and we'll change the filter fields to enable sorting and filtering by the language field. We'll have associated changes to the querying, filtering and ordering in our model.

Site Front-end Form

We'll add a field to capture the language, and we'll pre-populate it with language that the form is being displayed in.

Site Helloworld display pages in general

With our site now multilingual, we'll ensure that if the URL to the page we're on has a language component then we will restrict the record or records displayed to just that language.

It's important to realise that the language displayed isn't determined by the default language of the user who is logged on, but rather by the URL which the user has entered. For example, a user whose chosen language is English might be browsing the site in French. When the user initially accesses the site the language which Joomla selects will depend on a number of factors, eg the default language of the site, whether the user is already logged on, or the user's browser settings, but thereafter the user controls what language is displayed through the links which he/she accesses.

Joomla gives an easy way to determine if the site is multilingual, and if so, what language the page being displayed is in:

JLanguageMultilang::isEnabled() returns true if the site is multilingual, false otherwise

JFactory::getLanguage()->getTag() returns the string which defines the language of the page. (The language segment of the URL has a slightly different form – browse the Joomla Languages table in phpmyadmin to see the differences).

So we change our model code we perform that check and restrict the SQL query to the appropriate language (remembering that the language field can be "*" signifying all languages).

In addition, we ensure that where we output links to helloworld records, we also perform that check and add &lang=xxx to our JRoute::_ call.

Site Category List view

We'll change this to display not only the helloworld records with the given category, but also the subcategories, and display the each subcategory as a link to the same Category List view but with that subcategory id as the key.

Map and Ajax

Where we display text within our Javascript code it should now be language-specific. However we have already seen how to do this by passing our language strings to the Javascript code using the function JText::script, as we saw in our code for submitbutton.js and the admin helloworld view in the tutorial step Adding verifications. So we'll not add anything more here.

In our route helper function to get the Ajax URL we're not now going to be able to just simply look for a menuitem with a specific alias. Instead we'll have to consider the language, and select the menuitem item with alias "message", "salutation" or "mensaje", depending upon the language.

There are various ways to solve this, but the one we'll adopt is that we'll find the language we're working in (ie associated with the current menuitem we're on), and then look for another menuitem which has the note field set to "Ajax" and the same language.

Router

Our parse() and build() routines of our custom router will need to be changed to support the URL structure defined above.

Language translation files

Language translation strings for the other languages will have to be provided - you'll have to do the translations yourself! - and links included within the helloworld component XML manifest file. These should be defined for both the admin and site strings.

Updating the Database

Add the language field to the database record:

admin/sql/install.mysql.utf8.sql

DROP TABLE IF EXISTS `#__helloworld`;

CREATE TABLE `#__helloworld` (
	`id`       INT(11)     NOT NULL AUTO_INCREMENT,
	`asset_id` INT(10)     NOT NULL DEFAULT '0',
	`created`  DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00',
	`created_by`  INT(10) UNSIGNED NOT NULL DEFAULT '0',
	`greeting` VARCHAR(25) NOT NULL,
	`alias`  VARCHAR(40)  NOT NULL DEFAULT '',
	`language`  CHAR(7)  NOT NULL DEFAULT '*',
	`published` tinyint(4) NOT NULL DEFAULT '1',
	`catid`	    int(11)    NOT NULL DEFAULT '0',
	`params`   VARCHAR(1024) NOT NULL DEFAULT '',
	`image`   VARCHAR(1024) NOT NULL DEFAULT '',
	`latitude` DECIMAL(9,7) NOT NULL DEFAULT 0.0,
	`longitude` DECIMAL(10,7) NOT NULL DEFAULT 0.0,
	PRIMARY KEY (`id`)
)
	ENGINE =MyISAM
	AUTO_INCREMENT =0
	DEFAULT CHARSET =utf8;

CREATE UNIQUE INDEX `aliasindex` ON `#__helloworld` (`alias`, `catid`);

INSERT INTO `#__helloworld` (`greeting`,`alias`,`language`) VALUES
('Hello World!','hello-world','en-GB'),
('Goodbye World!','goodbye-world','en-GB');

New SQL update file: /admin/sql/updates/mysql/0.0.21.sql

ALTER TABLE `#__helloworld` ADD COLUMN `language` CHAR(7) NOT NULL DEFAULT '*' AFTER `alias`;

DROP INDEX `aliasindex` on `#__helloworld`;
CREATE UNIQUE INDEX `aliasindex` ON `#__helloworld` (`alias`, `catid`);

Admin Management

This time our view file is fine, but all the other source code associated with this administration functionality will need to be updated.

Our XML definition of the form for editing helloworld records: admin/models/forms/helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<form
				addrulepath="/administrator/components/com_helloworld/models/rules"
>
	<fieldset
				name="details"
				label="COM_HELLOWORLD_HELLOWORLD_DETAILS"
	>
		<field
				name="id"
				type="hidden"
				/>
		<field
				name="greeting"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_GREETING_DESC"
				size="40"
				class="inputbox validate-greeting"
				validate="greeting"
				required="true"
				default=""
				/>
		<field 
				name="alias" 
				type="text" 
				label="JFIELD_ALIAS_LABEL"
				description="JFIELD_ALIAS_DESC"
				hint="JFIELD_ALIAS_PLACEHOLDER"
				size="40" 
				/>
		<field
				name="catid"
				type="category"
				extension="com_helloworld"
				class="inputbox"
				default=""
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC"
				required="true"
		>
				<option value="0">JOPTION_SELECT_CATEGORY</option>
		</field>
		<field
				name="latitude"
				type="number"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_DESC"
				min="-90.0"
				max="90.0"
				class="inputbox"
				required="true"
				default="0.0"
				/>
		<field
				name="longitude"
				type="number"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_DESC"
				min="-180.0"
				max="180.0"
				class="inputbox"
				required="true"
				default="0.0"
				/>
		<field
				name="language" 
				type="contentlanguage" 
				label="JFIELD_LANGUAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC"
				>
				<option value="*">JALL</option>
		</field>
	</fieldset>
	<fields name="imageinfo">
		<fieldset
			name="image-info"
			label="COM_HELLOWORLD_IMAGE_FIELDS"
		>
			<field
				name="image"
				type="media"
				preview="tooltip"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_DESC" />
			<field name="alt"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_DESC"
				size="30"/>
			<field name="caption"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_DESC"
				size="30"/>
		</fieldset>
	</fields>
	<fields name="params">
		<fieldset
				name="params"
				label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS"
		>
			<field
					name="show_category"
					type="list"
					label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
					description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
					default=""
			>
				<option value="">JGLOBAL_USE_GLOBAL</option>
				<option value="0">JHIDE</option>
				<option value="1">JSHOW</option>
			</field>
		</fieldset>
	</fields>
	<fieldset
			name="accesscontrol"
			label="COM_HELLOWORLD_FIELDSET_RULES"
	>
    	<field
				name="asset_id"
				type="hidden"
				filter="unset"
				/>
    	<field
				name="rules"
				type="rules"
				label="COM_HELLOWORLD_FIELD_RULES_LABEL"
				filter="rules"
				validate="rules"
				class="inputbox"
				component="com_helloworld"
				section="message"
				/>
    </fieldset>
</form>

Our helloworlds model, for providing the data for our display of helloworld records: admin/models/helloworlds.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorldList Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorlds extends JModelList
{
	/**
	 * Constructor.
	 *
	 * @param   array  $config  An optional associative array of configuration settings.
	 *
	 * @see     JController
	 * @since   1.6
	 */
	public function __construct($config = array())
	{
		if (empty($config['filter_fields']))
		{
			$config['filter_fields'] = array(
				'id',
				'greeting',
				'author',
				'created',
				'language',
				'published'
			);
		}

		parent::__construct($config);
	}

	/**
	 * Method to build an SQL query to load the list data.
	 *
	 * @return      string  An SQL query
	 */
	protected function getListQuery()
	{
		// Initialize variables.
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);

		// Create the base select statement.
		$query->select('a.id as id, a.greeting as greeting, a.published as published, a.created as created, 
			  a.image as imageInfo, a.latitude as latitude, a.longitude as longitude, a.alias as alias, a.language as language')
			  ->from($db->quoteName('#__helloworld', 'a'));

		// Join over the categories.
		$query->select($db->quoteName('c.title', 'category_title'))
			->join('LEFT', $db->quoteName('#__categories', 'c') . ' ON c.id = a.catid');
        
		// Join with users table to get the username of the author
		$query->select($db->quoteName('u.username', 'author'))
			->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.created_by');

		// Join with languages table to get the language title and image to display
		// Put these into fields called language_title and language_image so that 
		// we can use the little com_content layout to display the map symbol
		$query->select($db->quoteName('l.title', 'language_title') . "," .$db->quoteName('l.image', 'language_image'))
			->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON l.lang_code = a.language');

		// Filter: like / search
		$search = $this->getState('filter.search');

		if (!empty($search))
		{
			$like = $db->quote('%' . $search . '%');
			$query->where('greeting LIKE ' . $like);
		}

		// Filter by published state
		$published = $this->getState('filter.published');

		if (is_numeric($published))
		{
			$query->where('a.published = ' . (int) $published);
		}
		elseif ($published === '')
		{
			$query->where('(a.published IN (0, 1))');
		}

		// Filter by language, if the user has set that in the filter field
		$language = $this->getState('filter.language');
		if ($language)
		{
			$query->where('a.language = ' . $db->quote($language));
		}

		// Add the list ordering clause.
		$orderCol	= $this->state->get('list.ordering', 'greeting');
		$orderDirn 	= $this->state->get('list.direction', 'asc');

		$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));

		return $query;
	}
}

Our layout file: admin/views/helloworlds/tmpl/default.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted Access');

use Joomla\Registry\Registry;

JHtml::_('formbehavior.chosen', 'select');

$listOrder     = $this->escape($this->state->get('list.ordering'));
$listDirn      = $this->escape($this->state->get('list.direction'));
?>
<form action="index.php?option=com_helloworld&view=helloworlds" method="post" id="adminForm" name="adminForm">
	<div id="j-sidebar-container" class="span2">
		<?php echo JHtmlSidebar::render(); ?>
	</div>
	<div id="j-main-container" class="span10">
        <div class="row-fluid">
            <div class="span6">
                <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_FILTER'); ?>
                <?php
                    echo JLayoutHelper::render(
                        'joomla.searchtools.default',
                        array('view' => $this)
                    );
                ?>
            </div>
        </div>
        <table class="table table-striped table-hover">
            <thead>
            <tr>
                <th width="1%"><?php echo JText::_('COM_HELLOWORLD_NUM'); ?></th>
                <th width="2%">
                    <?php echo JHtml::_('grid.checkall'); ?>
                </th>
                <th width="15%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLDS_NAME', 'greeting', $listDirn, $listOrder); ?>
                </th>
                <th width="15%">
                    <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_POSITION'); ?>
                </th>
                <th width="15%">
                    <?php echo JText::_('COM_HELLOWORLD_HELLOWORLDS_IMAGE'); ?>
                </th>
                <th width="15%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_AUTHOR', 'author', $listDirn, $listOrder); ?>
                </th>
                <th width="15%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_LANGUAGE', 'language', $listDirn, $listOrder); ?>
                </th>
                <th width="15%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_CREATED_DATE', 'created', $listDirn, $listOrder); ?>
                </th>
                <th width="5%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_PUBLISHED', 'published', $listDirn, $listOrder); ?>
                </th>
                <th width="2%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_ID', 'id', $listDirn, $listOrder); ?>
                </th>
            </tr>
            </thead>
            <tfoot>
                <tr>
                    <td colspan="5">
                        <?php echo $this->pagination->getListFooter(); ?>
                    </td>
                </tr>
            </tfoot>
            <tbody>
                <?php if (!empty($this->items)) : ?>
                    <?php foreach ($this->items as $i => $row) :
                        $link = JRoute::_('index.php?option=com_helloworld&task=helloworld.edit&id=' . $row->id);
                        $row->image = new Registry;
                        $row->image->loadString($row->imageInfo);
                    ?>
                        <tr>
                            <td><?php echo $this->pagination->getRowOffset($i); ?></td>
                            <td>
                                <?php echo JHtml::_('grid.id', $i, $row->id); ?>
                            </td>
                            <td>
                                <a href="<?php echo $link; ?>" title="<?php echo JText::_('COM_HELLOWORLD_EDIT_HELLOWORLD'); ?>">
                                    <?php echo $row->greeting; ?>
                                </a>
                                <span class="small break-word">
                                	<?php echo JText::sprintf('JGLOBAL_LIST_ALIAS', $this->escape($row->alias)); ?>
                                </span>
                                <div class="small">
                                    <?php echo JText::_('JCATEGORY') . ': ' . $this->escape($row->category_title); ?>
                                </div>
                            </td>
                            <td align="center">
                                <?php echo "[" . $row->latitude . ", " . $row->longitude . "]"; ?>
                            </td>
                            <td align="center">
                                <?php
                                    $caption = $row->image->get('caption') ? : '' ;
                                    $src = JURI::root() . ($row->image->get('image') ? : '' );
                                    $html = '<p class="hasTooltip" style="display: inline-block" data-html="true" data-toggle="tooltip" data-placement="right" title="<img width=\'100px\' height=\'100px\' src=\'%s\'>">%s</p>';
                                    echo sprintf($html, $src, $caption);  ?>
                            </td>
                            <td align="center">
                                <?php echo $row->author; ?>
                            </td>
                            <td align="center">
                                <?php echo JLayoutHelper::render('joomla.content.language', $row); ?>
                            </td>
                            <td align="center">
                                <?php echo substr($row->created, 0, 10); ?>
                            </td>
                            <td align="center">
                                <?php echo JHtml::_('jgrid.published', $row->published, $i, 'helloworlds.', true, 'cb'); ?>
                            </td>
                            <td align="center">
                                <?php echo $row->id; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>
        <input type="hidden" name="task" value=""/>
        <input type="hidden" name="boxchecked" value="0"/>
        <?php echo JHtml::_('form.token'); ?>
    </div>
</form>

Our XML definition of the filter fields: admin/models/forms/filter_helloworlds.xml

<?xml version="1.0" encoding="utf-8"?>
<form>
	<fields name="filter">
		<field
			name="search"
			type="text"
			label="COM_BANNERS_SEARCH_IN_TITLE"
			hint="JSEARCH_FILTER"
			class="js-stools-search-string"
		/>
		<field
			name="published"
			type="status"
			label="JOPTION_SELECT_PUBLISHED"
			statuses="0,1"
			description="JOPTION_SELECT_PUBLISHED_DESC"
			onchange="this.form.submit();"
			>
			<option value="">JOPTION_SELECT_PUBLISHED</option>
		</field>
		<field
			name="language"
			type="contentlanguage"
			label="JOPTION_FILTER_LANGUAGE"
			description="JOPTION_FILTER_LANGUAGE_DESC"
			onchange="this.form.submit();"
			>
			<option value="">JOPTION_SELECT_LANGUAGE</option>
			<option value="*">JALL</option>
		</field>
	</fields>
	<fields name="list">
		<field
			name="fullordering"
			type="list"
			label="COM_HELLOWORLD_LIST_FULL_ORDERING"
			description="COM_HELLOWORLD_LIST_FULL_ORDERING_DESC"
			onchange="this.form.submit();"
			default="greeting ASC"
			>
			<option value="">JGLOBAL_SORT_BY</option>
			<option value="greeting ASC">COM_HELLOWORLD_ORDERING_ASC</option>
			<option value="greeting DESC">COM_HELLOWORLD_ORDERING_DESC</option>
			<option value="id ASC">JGRID_HEADING_ID_ASC</option>
			<option value="id DESC">JGRID_HEADING_ID_DESC</option>
			<option value="published ASC">COM_HELLOWORLD_PUBLISHED_ASC</option>
			<option value="published DESC">COM_HELLOWORLD_PUBLISHED_DESC</option>
			<option value="author ASC">COM_HELLOWORLD_AUTHOR_ASC</option>
			<option value="author DESC">COM_HELLOWORLD_AUTHOR_DESC</option>
			<option value="created ASC">COM_HELLOWORLD_CREATED_ASC</option>
			<option value="created DESC">COM_HELLOWORLD_CREATED_DESC</option>
			<option value="language ASC">COM_HELLOWORLD_LANGUAGE_ASC</option>
			<option value="language DESC">COM_HELLOWORLD_LANGUAGE_DESC</option>
		</field>
		<field
			name="limit"
			type="limitbox"
			class="input-mini"
			default="25"
			label="COM_CONTENT_LIST_LIMIT"
			description="COM_HELLOWORLD_LIST_LIMIT_DESC"
			onchange="this.form.submit();"
		/>
	</fields>
</form>

Site Front-end Form

Our XML form definition (we'll not offer the option of "*" to represent all languages): site/models/forms/add-form.xml

<?xml version="1.0" encoding="utf-8"?>
<form
    addrulepath="/administrator/components/com_helloworld/models/rules"
    >
    <fieldset
				name="details"
				label="COM_HELLOWORLD_HELLOWORLD_DETAILS"
	>
		<field
				name="id"
				type="hidden"
				/>
		<field
				name="greeting"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_GREETING_DESC"
				size="40"
				class="inputbox"
				validate="greeting"
				required="true"
				hint="COM_HELLOWORLD_HELLOWORLD_GREETING_HINT"
				default=""
				/>
		<field 
				name="alias" 
				type="text" 
				label="JFIELD_ALIAS_LABEL"
				description="JFIELD_ALIAS_DESC"
				hint="JFIELD_ALIAS_PLACEHOLDER"
				size="40" 
				/>
		<field
				name="catid"
				type="category"
				extension="com_helloworld"
				class="inputbox"
				default=""
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC"
				required="true"
				>
				<option value="0">JOPTION_SELECT_CATEGORY</option>
		</field>
		<fields name="imageinfo" label="COM_HELLOWORLD_HELLOWORLD_IMAGE_LABEL">
			<field
				name="image"
				type="file"
				label="COM_HELLOWORLD_HELLOWORLD_PICTURE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_PICTURE_DESC" 
				accept="image/*"
				>
			</field>
			<field
 				name="caption"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_CAPTION_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_CAPTION_DESC"
				size="40"
				class="inputbox"
				>
			</field>
			<field
				name="alt"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_ALTTEXT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_ALTTEXT_DESC"
				size="40"
				class="inputbox"
				>
			</field>
		</fields>
		<field
				name="language"
				type="contentlanguage"
				label="JFIELD_LANGUAGE_LABEL"
				description="JFIELD_LANGUAGE_DESC"
				class="inputbox">
		</field>
		<field
				name="message"
				type="textarea"
				rows="5"
				cols="80"
				label="COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC"
				hint="COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT"
				required="true"
				>
        </field>
        <field
				name="captcha"
				type="captcha"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC"
				validate="captcha"
                >
		</field>
        <fields name="params">
            <field
                    name="show_category"
                    type="list"
                    label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
                    description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
                    default=""
                    useglobal="true"
            >
                <option value="0">JHIDE</option>
                <option value="1">JSHOW</option>
            </field>
        </fields>
    </fieldset>
</form>

We need to set the default language for the front-end form dynamically: site/views/form/view.html.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld View
 * This is the site view presenting the user with the ability to add a new Helloworld record
 * 
 */
class HelloWorldViewForm extends JViewLegacy
{

	protected $form = null;
	protected $canDo;

	/**
	 * Display the Hello World view
	 *
	 * @param   string  $tpl  The name of the layout file to parse.
	 *
	 * @return  void
	 */
	public function display($tpl = null)
	{
		// Get the form to display
		$this->form = $this->get('Form');
		// Get the javascript script file for client-side validation
		$this->script = $this->get('Script'); 

		// Propose current language as default
		if (JLanguageMultilang::isEnabled())
		{
			$lang = JFactory::getLanguage()->getTag();
			$this->form->setFieldAttribute('language', 'default', $lang);
		}

		// Check that the user has permissions to create a new helloworld record
		$this->canDo = JHelperContent::getActions('com_helloworld');
		if (!($this->canDo->get('core.create'))) 
		{
			$app = JFactory::getApplication(); 
			$app->enqueueMessage(JText::_('JERROR_ALERTNOAUTHOR'), 'error');
			$app->setHeader('status', 403, true);
			return;
		}
        
		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			throw new Exception(implode("\n", $errors), 500);
		}

		// Call the parent display to display the layout file
		parent::display($tpl);

		// Set properties of the html document
		$this->setDocument();
	}

	/**
	 * Method to set up the html document properties
	 *
	 * @return void
	 */
	protected function setDocument() 
	{
		$document = JFactory::getDocument();
		$document->setTitle(JText::_('COM_HELLOWORLD_HELLOWORLD_CREATING'));
		$document->addScript(JURI::root() . $this->script);
		$document->addScript(JURI::root() . "/administrator/components/com_helloworld"
		                                  . "/views/helloworld/submitbutton.js");
		JText::script('COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE');
	}
}

Site Helloworld Display

In our site helloworld display we need to ensure that our SQL queries having filtering by language in the where clause, and that the JRoute::_() function calls have &lang=xxx included; these only affect our model code.

When we're performing the map search we'll also have to select the category id, as we'll be using that in the URL if the user has got to the helloworld record via the Category List.

site/models/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

JLoader::register('HelloworldHelperRoute', JPATH_ROOT . '/components/com_helloworld/helpers/route.php');

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelItem
{
	/**
	 * @var object item
	 */
	protected $item;

	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @return	void
	 * @since	2.5
	 */
	protected function populateState()
	{
		// Get the message id
		$jinput = JFactory::getApplication()->input;
		$id     = $jinput->get('id', 1, 'INT');
		$this->setState('message.id', $id);

		// Load the parameters.
		$this->setState('params', JFactory::getApplication()->getParams());
		parent::populateState();
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

	/**
	 * Get the message
	 * @return object The message to be displayed to the user
	 */
	public function getItem()
	{
		if (!isset($this->item)) 
		{
			$id    = $this->getState('message.id');
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.greeting, h.params, h.image as image, c.title as category, h.latitude as latitude, h.longitude as longitude')
				  ->from('#__helloworld as h')
				  ->leftJoin('#__categories as c ON h.catid=c.id')
				  ->where('h.id=' . (int)$id);

			if (JLanguageMultilang::isEnabled())
			{
				$lang = JFactory::getLanguage()->getTag();
				$query->where('h.language IN ("*","' . $lang . '")');
			}

			$db->setQuery((string)$query);
		
			if ($this->item = $db->loadObject()) 
			{
				// Load the JSON string
				$params = new JRegistry;
				$params->loadString($this->item->params, 'JSON');
				$this->item->params = $params;

				// Merge global params with item params
				$params = clone $this->getState('params');
				$params->merge($this->item->params);
				$this->item->params = $params;

				// Convert the JSON-encoded image info into an array
				$image = new JRegistry;
				$image->loadString($this->item->image, 'JSON');
				$this->item->imageDetails = $image;
			}
			else
			{
				throw new Exception('Helloworld id not found', 404);
			}
		}
		return $this->item;
	}

	public function getMapParams()
	{
		if ($this->item) 
		{
			$url = HelloworldHelperRoute::getAjaxURL();
			$this->mapParams = array(
				'latitude' => $this->item->latitude,
				'longitude' => $this->item->longitude,
				'zoom' => 10,
				'greeting' => $this->item->greeting,
				'ajaxurl' => $url
			);
			return $this->mapParams; 
		}
		else
		{
			throw new Exception('No helloworld details available for map', 500);
		}
	}

	public function getMapSearchResults($mapbounds)
	{
		try 
		{
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.id, h.alias, h.catid, h.greeting, h.latitude, h.longitude')
			   ->from('#__helloworld as h')
			   ->where('h.latitude > ' . $mapbounds['minlat'] . 
				' AND h.latitude < ' . $mapbounds['maxlat'] .
				' AND h.longitude > ' . $mapbounds['minlng'] .
				' AND h.longitude < ' . $mapbounds['maxlng']);

			if (JLanguageMultilang::isEnabled())
			{
				$lang = JFactory::getLanguage()->getTag();
				$query->where('h.language IN ("*","' . $lang . '")');
			}

			$db->setQuery($query);
			$results = $db->loadObjectList(); 
		}
		catch (Exception $e)
		{
			$msg = $e->getMessage();
			JFactory::getApplication()->enqueueMessage($msg, 'error'); 
			$results = null;
		}

		if (JLanguageMultilang::isEnabled())
		{
			$query_lang = "&lang={$lang}";
		}
		else
		{
			$query_lang = "";
		}

		for ($i = 0; $i < count($results); $i++) 
		{
			$results[$i]->url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $results[$i]->id . 
				":" . $results[$i]->alias . '&catid=' . $results[$i]->catid . $query_lang);
		}

		return $results; 
	}
}

Site Category List View

In the view we'll display the category name and the subcategories, so we'll need to include calls to get these items from the model.

site/views/category/view.html.php

<?php
/*
 * View file for the view which displays a list of helloworld messages in a given category
 */
 
defined('_JEXEC') or die;

class HelloworldViewCategory extends JViewLegacy
{
	public function display($tpl = null)
	{
		$this->categoryName = $this->get("CategoryName");

		$this->items = $this->get('Items');
		$this->pagination = $this->get('Pagination');
		$this->state = $this->get('State');
		$this->filterForm    	= $this->get('FilterForm');
		$this->activeFilters 	= $this->get('ActiveFilters');

		$this->subcategories = $this->get('Subcategories');

		parent::display($tpl);
	}
}

And our updated model code: site/models/category.php

<?php
/**
 * Model for displaying the helloworld messages in a given category
 */

defined('_JEXEC') or die;

class HelloworldModelCategory extends JModelList
{
	public function __construct($config = array())
	{
		if (empty($config['filter_fields']))
		{
			$config['filter_fields'] = array(
				'id',
				'greeting',
				'alias',
			);
		}

		parent::__construct($config);
	}
    
	protected function populateState($ordering = null, $direction = null)
	{
		parent::populateState($ordering, $direction);
        
		$app = JFactory::getApplication('site');
		$catid = $app->input->getInt('id');

		$this->setState('category.id', $catid);
	}
    
	protected function getListQuery()
	{
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);

		$catid = $this->getState('category.id'); 
		$query->select('id, greeting, alias, catid')
			->from($db->quoteName('#__helloworld'))
			->where('catid = ' . $catid);

		if (JLanguageMultilang::isEnabled())
		{
			$lang = JFactory::getLanguage()->getTag();
			$query->where('language IN ("*","' . $lang . '")');
		}

		$orderCol	= $this->state->get('list.ordering', 'greeting');
		$orderDirn 	= $this->state->get('list.direction', 'asc');

		$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));

		return $query;	
	}

	public function getCategoryName()
	{
		$catid = $this->getState('category.id'); 
		$categories = JCategories::getInstance('Helloworld', array());
		$categoryNode = $categories->get($catid);   
		return $categoryNode->title; 
	}
    
	public function getSubcategories()
	{
		$catid = $this->getState('category.id'); 
		$categories = JCategories::getInstance('Helloworld', array());
		$categoryNode = $categories->get($catid);
		$subcats = $categoryNode->getChildren(); 
        
		$lang = JFactory::getLanguage()->getTag();
		if (JLanguageMultilang::isEnabled() && $lang)
		{
			$query_lang = "&lang={$lang}";
		}
		else
		{
			$query_lang = '';
		}
        
		foreach ($subcats as $subcat)
		{
			$subcat->url = JRoute::_("index.php?view=category&id=" . $subcat->id . $query_lang);
		}
		return $subcats;
	}
}

The code above introduces some of the APIs around Joomla Categories.

$categories = JCategories::getInstance('Helloworld', array());

gives us a handle on the Categories object related to Helloworld categories, but we have to go via a HelloworldCategory class, defined in the code below.

$categoryNode = $categories->get($catid);

returns us the CategoryNode object (which relates to a single Category) associated with the id we pass in (or 'root' for the root category). The class definition of HelloworldCategories we put into a new helper file: site/helpers/category.php

<?php
/**
 * Class definition for HelloworldCategories
 */

defined('_JEXEC') or die;

class HelloworldCategories extends JCategories
{

	public function __construct($options = array())
	{
		$options['table'] = '#__helloworld';
		$options['extension'] = 'com_helloworld';

		parent::__construct($options);
	}
}

Finally, the layout file is updated to output the category name and the subcategories, as well as including the &lang=xxx part in the URL of the helloworld records. site/views/category/tmpl/default.php

<?php
/**
 * Layout file for displaying helloworld messages belonging to a given category
 */

defined('_JEXEC') or die;

JHtml::_('formbehavior.chosen', 'select');

$listOrder     = $this->escape($this->state->get('list.ordering'));
$listDirn      = $this->escape($this->state->get('list.direction'));
$lang = JFactory::getLanguage()->getTag();
if (JLanguageMultilang::isEnabled() && $lang)
{
    $query_lang = "&lang={$lang}";
}
else
{
    $query_lang = "";
}
?>
<form action="#" method="post" id="adminForm" name="adminForm">
<h1><?php echo $this->categoryName; ?></h1>
<div id="j-main-container" class="span10">
    <div class="row-fluid">
        <div class="span10">
            <?php
                echo JLayoutHelper::render(
                    'joomla.searchtools.default',
                    array('view' => $this, 'searchButton' => false)
                );
            ?>
        </div>
    </div>
<table class="table table-striped table-hover">
    <thead>
    <tr>
        <th width="5%"><?php echo JText::_('JGLOBAL_NUM'); ?></th>
        <th width="20%">
            <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL', 'greeting', $listDirn, $listOrder); ?>
        </th>
        <th width="20%">
            <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLD_ALIAS_LABEL', 'alias', $listDirn, $listOrder); ?>
        </th>
        <th width="20%">
            <?php echo JText::_('COM_HELLOWORLD_HELLOWORLD_FIELD_URL_LABEL'); ?>
        </th>
        <th width="5%">
            <?php echo JHtml::_('searchtools.sort', 'JGLOBAL_FIELD_ID_LABEL', 'id', $listDirn, $listOrder); ?>
        </th>
    </tr>
    </thead>
    <tfoot>
        <tr>
            <td colspan="5">
                <?php echo $this->pagination->getListFooter(); ?>
            </td>
        </tr>
    </tfoot>
    <tbody>
        <?php if (!empty($this->items)) : ?>
            <?php foreach ($this->items as $i => $row) : 
                $url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $row->id . ':' . $row->alias . '&catid=' . $row->catid . $query_lang);
                ?>
                <tr>
                    <td align="center"><?php echo $this->pagination->getRowOffset($i); ?></td>
                    <td align="center"><?php echo $row->greeting; ?></td>
                    <td align="center"><?php echo $row->alias; ?></td>
                    <td align="center"><a href="<?php echo $url; ?>"><?php echo $url; ?></a></td>
                    <td align="center"><?php echo $row->id; ?></td>
                </tr>
            <?php endforeach; ?>
        <?php endif; ?>
    </tbody>
</table>
<h1><?php echo JText::_('COM_HELLOWORLD_HEADER_SUBCATEGORIES'); ?></h1>
<?php foreach ($this->subcategories as $subcategory) : ?>
    <h3><a href="<?php echo $subcategory->url; ?>"> <?php echo $subcategory->title; ?> </a></h3>
    <p><?php echo $subcategory->description; ?></p>
<?php endforeach; ?>
</div>
</form>

Map and Ajax

We change our method of finding the URL for our Ajax call:

site/helpers/route.php

<?php

defined('_JEXEC') or die;

/**
 * Helloworld Component Helper file for generating the URL Routes
 *
 */
class HelloworldHelperRoute
{
	/**
	 * When the Helloworld message is displayed then there is also shown a map with a Search Here button.
	 * This function generates the URL which the Ajax call will use to perform the search. 
	 * 
	 */
	public static function getAjaxURL()
	{
		if (!JLanguageMultilang::isEnabled())
		{
			return null;
		}
        
		$lang = JFactory::getLanguage()->getTag();
		$app  = JFactory::getApplication();
		$sitemenu= $app->getMenu();
		$thismenuitem = $sitemenu->getActive();

		// if we haven't got an active menuitem, or we're currently on a menuitem 
		// with view=category or note = "Ajax", then just stay on it
		if (!$thismenuitem || strpos($thismenuitem->link, "view=category") !== false || $thismenuitem->note == "Ajax")
		{
			return null;
		}

		// look for a menuitem with the right language, and a note field of "Ajax"
		$menuitem = $sitemenu->getItems(array('language','note'), array($lang, "Ajax"));
		if ($menuitem)
		{
			$itemid = $menuitem[0]->id; 
			$url = JRoute::_("index.php?Itemid=$itemid&view=helloworld&format=json");
			return $url;
		}
		else
		{
			return null;
		}
	}
}

Router

Our custom router code must be written to support the mapping to/from the SEF URLs, as described in the table below.

Active Menu View Ids URL format
messages category category id messages/37:messages/38:formal – category hierarchy
messages helloworld category id and helloworld id messages/37:messages/38:formal/43:good-morning – category hierarchy + helloworld id:alias
message helloworld category id and helloworld id message/id:alias - helloworld id:alias

The build() function finds the active menuitem, then based on the view builds up the URL segments from the ids.

The parse() function also finds the active menuitem (which is determined by the early Joomla routing functionality) then matches the URL segments against categories in the helloworld category hierarchy, taking the last segment as a helloworld record if it can't find a matching category. These functions rely on the configuration being built under the top-level language-specific categories as described above. It's instructive to build data under other top-level categories, see how the functionality breaks, and try to understand why it is failing.

site/router.php

<?php

defined('_JEXEC') or die;

class HelloworldRouter implements JComponentRouterInterface
{

	public function build(&$query)
	{
		$segments = array();

		if (!JLanguageMultilang::isEnabled() || !isset($query['view']))
		{
			return $segments;
		}

		$lang = JFactory::getLanguage()->getTag();
		$app  = JFactory::getApplication();
        
		// get the menu item that this call to build() relates to
		if (!isset($query['Itemid']))
		{
			return $segments;
		}
		$sitemenu = $app->getMenu();
		$thisMenuitem = $sitemenu->getItem($query['Itemid']);

		if ($thisMenuitem->language != $lang)
		{
			return $segments;
		}
        
		if ($thisMenuitem->note == "Ajax")
		{   
			// We're on the /message menuitem. 
			// Check we've got the right parameters then set url segment = id : alias
			if ($query['view'] == "helloworld" && isset($query['id']))
			{
				// we'll support the passed id being in the form id:alias
				$segments[] = $query['id'];

				unset($query['id']);
				unset($query['catid']);
			}
		}
		else
		{
			// assume we're on the /messages menuitem
			if (($query['view'] == "category") && isset($query['id']))
			{
				// set this part of the url to be of the form /subcat1/subcat2/...
				$pathSegments = $this->getCategorySegments($query['id']);
				if ($pathSegments)
				{
					$segments = $pathSegments;
					unset($query['id']);
				}
			}
			elseif ($query['view'] == "helloworld" && isset($query['catid']) && isset($query['id']))
			{
				// set this part of the url to be of the form /subcat1/subcat2/.../hello-world 
				$pathSegments = $this->getCategorySegments($query['catid']);
				if ($pathSegments)
				{
					$segments = $pathSegments;
				}

				$segments[] = $query['id'];

				unset($query['id']);
				unset($query['catid']);
			}
		}

		unset($query['view']);
		return $segments;
	}

	/*
	 * This function take a category id and finds the path from that category to the root of the category tree
	 * The path returned from getPath() is an associative array of key = category id, value = id:alias
	 * If no valid category is found from the passed-in category id then null is returned. 
	 */
     
	private function getCategorySegments($catid)
	{
		$categories = JCategories::getInstance('Helloworld', array());
		$categoryNode = $categories->get($catid);
		if ($categoryNode)
		{
			$path = $categoryNode->getPath();

			return $path;
		}
		else
		{
			return null;
		}
	}

	public function parse(&$segments)
	{
		$vars = array();
		$nSegments = count($segments);
        
		$app  = JFactory::getApplication();
		$sitemenu = $app->getMenu();
		$activeMenuitem = $sitemenu->getActive();
		if (!$activeMenuitem)
		{
			return vars;
		}
        
		if ($activeMenuitem->note == "Ajax")
		{
			// Expect 1 segment of the form id:alias for the helloworld record
			if ($nSegments == 1)
			{
				$vars['id'] = $segments[0];
				$vars['view'] = 'helloworld';
			}
		}
		else
		{
			// Try to match the categories in the segments, starting at the root
			$categories = JCategories::getInstance('Helloworld', array());
			$matchingCategory = $categories->get('root');
            
			// Go through the category tree, try to get a match between each segment
			// and the id:alias of one of the children
			// The last segment may be a category id:alias or a helloworld record id:alias
			for ($i=0; $i < $nSegments; $i++)
			{
				$children = $matchingCategory->getChildren();
				$matchingCategory = $this->match($children, $segments[$i]);
				if ($matchingCategory)
				{
					$catid = $matchingCategory->id;
					if ($i == $nSegments - 1)    // we're done, all segments are categories
					{
						$vars['view'] = 'category';
						$vars['id'] = $catid;
					}
				}
				else
				{
					if ($i == $nSegments - 1)   // all but last segment are categories
					{
						$vars['id'] = $segments[$i];
						$vars['view'] = 'helloworld';
					}
					else   // something went wrong - didn't get a match at this level
					{
						break;
					}
				}
			}
		}

		return $vars;
	}

	/*
	 * This function takes an array of categoryNode elements and a url segment
	 * It goes through the categoryNodes looking for the one whose id:alias matches the passed-in segment
	 *   and returns the matching categoryNode, or null if not found
	 */
	private function match($categoryNodes, $segment)
	{
		foreach ($categoryNodes as $categoryNode)
		{
			if ($segment == $categoryNode->id . ':' . $categoryNode->alias)
			{
				return $categoryNode;
			}
		}
		return null;
	}

	public function preprocess($query)
	{
		return $query;
	}
}

In the code above

$path = $categoryNode->getPath();

returns the path from the CategoryNode to the root category node, The path is an associative array with each element having key = category id and value = id:categoryAlias, and the first array element being the category nearest the root.


Updated Language Strings

You'll have to provide admin and site language files for the languages you decided to include in this step.

site/language/en-GB/en-GB.com_helloworld.ini

; add new message form
COM_HELLOWORLD_LEGEND_DETAILS="New Helloworld Message Details"
COM_HELLOWORLD_HELLOWORLD_CREATING="Add message"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Sorry, you have an error"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Message details"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Please specify the greeting to add"
COM_HELLOWORLD_HELLOWORLD_GREETING_HINT="Letters only!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="Please select the associated category"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_LABEL="Reason"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_DESC="Please say why you're adding this greeting"
COM_HELLOWORLD_HELLOWORLD_MESSAGE_HINT="No HTML tags!"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Spam protection"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Prove you're a real person!"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Display category or not?"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="Select if you want the category displayed too"
COM_HELLOWORLD_HELLOWORLD_IMAGE_LABEL="Image information"
COM_HELLOWORLD_HELLOWORLD_PICTURE_LABEL="Image file to upload"
COM_HELLOWORLD_HELLOWORLD_PICTURE_DESC="Select the file with the image to upload"
COM_HELLOWORLD_HELLOWORLD_CAPTION_LABEL="Caption"
COM_HELLOWORLD_HELLOWORLD_CAPTION_DESC="Text to use as a caption for the image"
COM_HELLOWORLD_HELLOWORLD_ALTTEXT_LABEL="Alt text"
COM_HELLOWORLD_HELLOWORLD_ALTTEXT_DESC="Text to display if image cannot be shown"
; save and cancel confirmation messages
COM_HELLOWORLD_ADD_SUCCESSFUL="New greeting successfully saved"
COM_HELLOWORLD_ADD_CANCELLED="New greeting cancelled ok"
; file upload error conditions
COM_HELLOWORLD_ERROR_FILEUPLOAD="PHP Error %s encountered when uploading file"
COM_HELLOWORLD_ERROR_FILETOOLARGE="Upload file exceeds max size configured in Joomla"
COM_HELLOWORLD_ERROR_BADFILENAME="Upload file has an invalid filename"
COM_HELLOWORLD_ERROR_FILE_EXISTS="Upload file already exists"
COM_HELLOWORLD_ERROR_UNABLE_TO_UPLOAD_FILE="Error creating uploaded file"
; helloworld greeting page
COM_HELLOWORLD_SEARCH_HERE_BUTTON="Search here"
; Ajax handling errors
COM_HELLOWORLD_ERROR_NO_RECORDS="Didn't get any records"
COM_HELLOWORLD_ERROR_NO_MAP_BOUNDS="Error: no map bounds"
; category view, search and ordering fields and headings
COM_HELLOWORLD_SORT_BY="Sort by ..."
COM_HELLOWORLD_GREETING_ASC="Greeting asc"
COM_HELLOWORLD_GREETING_DESC="Greeting desc"
COM_HELLOWORLD_ID_ASC="id asc"
COM_HELLOWORLD_ID_DESC="id desc"
COM_HELLOWORLD_ALIAS_ASC="alias asc"
COM_HELLOWORLD_ALIAS_DESC="alias desc"
COM_HELLOWORLD_HELLOWORLD_ALIAS_LABEL="Alias"
COM_HELLOWORLD_HELLOWORLD_FIELD_URL_LABEL="URL"
COM_HELLOWORLD_HEADER_SUBCATEGORIES="Subcategories"

admin/language/en-GB/en-GB.com_helloworld.ini

; Joomla! Project
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
; Note : All ini files need to be saved as UTF-8

COM_HELLOWORLD_ADMINISTRATION="HelloWorld - Administration"
COM_HELLOWORLD_ADMINISTRATION_CATEGORIES="HelloWorld - Categories"
COM_HELLOWORLD_NUM="#"
COM_HELLOWORLD_HELLOWORLDS_FILTER="Filters"
COM_HELLOWORLD_AUTHOR="Author"
COM_HELLOWORLD_LANGUAGE="Language"
COM_HELLOWORLD_CREATED_DATE="Created"
COM_HELLOWORLD_PUBLISHED="Published"
COM_HELLOWORLD_HELLOWORLDS_NAME="Name"
COM_HELLOWORLD_HELLOWORLDS_POSITION="Position"
COM_HELLOWORLD_HELLOWORLDS_IMAGE="Image"
COM_HELLOWORLD_ID="Id"

COM_HELLOWORLD_HELLOWORLD_CREATING="HelloWorld - Creating"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Details"
COM_HELLOWORLD_HELLOWORLD_EDITING="HelloWorld - Editing"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Some values are unacceptable"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="The category the messages belongs to"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_DESC="This message will be displayed"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_LABEL="Message"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Show category"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="If set to Show, the title of the message&rsquo;s category will show."
COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_LABEL="Latitude"
COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_DESC="Enter the position latitude, between -90 and +90 degrees"
COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_LABEL="Longitude"
COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_DESC="Enter the position longitude, between -180 and +180 degrees"
COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC="Select the appropriate language"
COM_HELLOWORLD_IMAGE_FIELDS="Image details"
COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_LABEL="Select image"
COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_DESC="Select an image from the library, or upload a new one"
COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_LABEL="Alt text"
COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_DESC="Alternative text (if image cannot be displayed)"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_LABEL="Caption"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_DESC="Provide a caption for the image"
COM_HELLOWORLD_HELLOWORLD_HEADING_GREETING="Greeting"
COM_HELLOWORLD_HELLOWORLD_HEADING_ID="Id"
COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT="HelloWorld manager: Edit Message"
COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW="HelloWorld manager: New Message"
COM_HELLOWORLD_MANAGER_HELLOWORLDS="HelloWorld manager"
COM_HELLOWORLD_EDIT_HELLOWORLD="Edit message"
COM_HELLOWORLD_N_ITEMS_DELETED_1="One message deleted"
COM_HELLOWORLD_N_ITEMS_DELETED_MORE="%d messages deleted"
COM_HELLOWORLD_N_ITEMS_PUBLISHED="%d message(s) published"
COM_HELLOWORLD_N_ITEMS_UNPUBLISHED="%d message(s) unpublished"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Add Hello World Greeting"
COM_HELLOWORLD_SUBMENU_MESSAGES="Messages"
COM_HELLOWORLD_SUBMENU_CATEGORIES="Categories"
COM_HELLOWORLD_CONFIGURATION="HelloWorld Configuration"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_LABEL="Messages settings"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC="Settings that will be applied to all messages by default"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Captcha"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Select Captcha to use on front end form"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL="User to email"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC="Select user to email when a new message is entered on front end"
COM_HELLOWORLD_FIELDSET_RULES="Message Permissions"
COM_HELLOWORLD_FIELD_RULES_LABEL="Permissions"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to edit this message?"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to delete this message?"
COM_HELLOWORLD_TAB_NEW_MESSAGE="New Message"
COM_HELLOWORLD_TAB_EDIT_MESSAGE="Message Details"
COM_HELLOWORLD_TAB_PARAMS="Parameters"
COM_HELLOWORLD_TAB_PERMISSIONS="Permissions"
COM_HELLOWORLD_TAB_IMAGE="Image"
COM_HELLOWORLD_LEGEND_DETAILS="Message Details"
COM_HELLOWORLD_LEGEND_PARAMS="Message Parameters"
COM_HELLOWORLD_LEGEND_PERMISSIONS="Message Permissions"
COM_HELLOWORLD_LEGEND_IMAGE="Image info"
; Column ordering in the Helloworlds view
COM_HELLOWORLD_ORDERING_ASC="Greeting ascending"
COM_HELLOWORLD_ORDERING_DESC="Greeting descending"
COM_HELLOWORLD_AUTHOR_ASC="Author ascending"
COM_HELLOWORLD_AUTHOR_DESC="Author descending"
COM_HELLOWORLD_CREATED_ASC="Creation date ascending"
COM_HELLOWORLD_CREATED_DESC="Creation date descending"
COM_HELLOWORLD_PUBLISHED_ASC="Unpublished first"
COM_HELLOWORLD_PUBLISHED_DESC="Published first"
COM_HELLOWORLD_LANGUAGE_ASC="Language ascending"
COM_HELLOWORLD_LANGUAGE_DESC="Language descending"

Packaging the Component

Contents of your code directory. Each file link below takes you to the step in the tutorial which has the latest version of that source code file.

Substitute in the highlighted <language> tags the file details for your chosen language(s).

helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="3.0" method="upgrade">

	<name>COM_HELLOWORLD</name>
	<!-- The following elements are optional and free of formatting constraints -->
	<creationDate>January 2018</creationDate>
	<author>John Doe</author>
	<authorEmail>john.doe@example.org</authorEmail>
	<authorUrl>http://www.example.org</authorUrl>
	<copyright>Copyright Info</copyright>
	<license>License Info</license>
	<!--  The version string is recorded in the components table -->
	<version>0.0.21</version>
	<!-- The description is optional and defaults to the name -->
	<description>COM_HELLOWORLD_DESCRIPTION</description>

	<!-- Runs on install/uninstall/update; New in 2.5 -->
	<scriptfile>script.php</scriptfile>

	<install> <!-- Runs on install -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Runs on uninstall -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
		</sql>
	</uninstall>
	<update> <!-- Runs on update; New since J2.5 -->
		<schemas>
			<schemapath type="mysql">sql/updates/mysql</schemapath>
		</schemas>
	</update>

	<!-- Site Main File Copy Section -->
	<!-- Note the folder attribute: This attribute describes the folder
		to copy FROM in the package to install therefore files copied
		in this section are copied from /site/ in the package -->
	<files folder="site">
		<filename>index.html</filename>
		<filename>helloworld.php</filename>
		<filename>controller.php</filename>
		<filename>router.php</filename>
		<folder>controllers</folder>
		<folder>views</folder>
		<folder>models</folder>
		<folder>helpers</folder>
	</files>

		<languages folder="site/language">
			<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.ini</language>
		</languages>

	<media destination="com_helloworld" folder="media">
		<filename>index.html</filename>
		<folder>images</folder>
		<folder>js</folder>
		<folder>css</folder>
	</media>

	<administration>
		<!-- Administration Menu Section -->
		<menu link='index.php?option=com_helloworld' img="../media/com_helloworld/images/tux-16x16.png">COM_HELLOWORLD_MENU</menu>
		<!-- Administration Main File Copy Section -->
		<!-- Note the folder attribute: This attribute describes the folder
			to copy FROM in the package to install therefore files copied
			in this section are copied from /admin/ in the package -->
		<files folder="admin">
			<!-- Admin Main File Copy Section -->
			<filename>index.html</filename>
			<filename>config.xml</filename>
			<filename>helloworld.php</filename>
			<filename>controller.php</filename>
			<filename>access.xml</filename>
			<!-- SQL files section -->
			<folder>sql</folder>
			<!-- tables files section -->
			<folder>tables</folder>
			<!-- models files section -->
			<folder>models</folder>
			<!-- views files section -->
			<folder>views</folder>
			<!-- controllers files section -->
			<folder>controllers</folder>
			<!-- helpers files section -->
			<folder>helpers</folder>
		</files>
		<languages folder="admin/language">
			<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
			<language tag="en-GB">en-GB/en-GB.com_helloworld.sys.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.sys.ini</language>
		</languages>
	</administration>

</extension>


Colaboradores