J3.x

Desarrollo de un Componente MVC/Agregar Asociaciones

From Joomla! Documentation

< J3.x:Developing an MVC Component
This page is a translated version of the page J3.x:Developing an MVC Component/Adding Associations and the translation is 100% complete.

Other languages:
English • ‎español • ‎français
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.

En este paso agregamos asociaciones multilingües.

Hay 3 vídeos (en inglés) que acompañan este paso, que cubren la vista de Helloworlds, la funcionalidad Edición y Asociaciones Multiligües, y los Cambios en el Lado Cliente.

Introducción

En este paso, mejoramos nuestro componente para admitir asociaciones multilingües, lo que nos permite asociar un elemento en un idioma con su equivalente en otro idioma. Esta funcionalidad es útil en varios aspectos:

  • Principalmente, ayuda a los administradores de un sitio multilingüe en el mantenimiento de los datos en los diferentes idiomas. Desde una perspectiva práctica de administración de datos, es esencial contar con algún mecanismo para realizar un seguimiento de qué elementos son equivalentes a otros en diferentes idiomas, e implementar asociaciones de Joomla en su componente significa que los administradores pueden usar los mismos procesos que utilizarían para otros elementos de Joomla Como artículos, menús y contactos.
  • En el lado cliente, un usuario puede hacer clic en el símbolo dentro del selector de idioma para ir al elemento equivalente en otro idioma.
  • In the html header Joomla outputs links to the equivalent items in other languages using the hreflang attribute. This enables search engines to find these equivalents, which they then use to try to deliver more pertinent search results to users.
  • Los traductores pueden usar el componente Asociaciones multilingües de Joomla para mostrar en una vista de lado a lado el elemento en el idioma de referencia y su equivalente en el idioma de destino, lo que facilita el proceso de traducción.

El desarrollo de esta funcionalidad implicará la introducción de algunas de las API en torno a la clase de formulario JForm de Joomla.

Functionalidad

Mejoramos nuestro componente helloworld para admitir asociaciones tanto para los registros (elementos) como para las categorías helloworld.

En el lado servidor, permitimos a los administradores establecer asociaciones como parte de la edición de elementos o categorías de helloworld, y extender la vista de lista para mostrar las asociaciones definidas. También habilitamos el uso del componente Asociaciones Multilingüe de Joomla.

En el lado cliente, ofrecemos a los usuarios la posibilidad de cambiar a la categoría o elemento de helloworld asociado cada vez que hacen clic en el símbolo de una bandera dentro del selector de idioma. Y nos aseguramos de que los enlaces hreflang en el elemento html head estén configurados correctamente.

Enfoque

Configuración Global

Para usar las asociaciones, el sitio debe configurarse como multilingüe, lo que básicamente significa que el complemento Filtro de Idioma del Sistema de Joomla debe estar habilitado, y esto dará como resultado que JLanguageMultilang::isEnabled() devuelva true. Asociada con este complemento hay una opción Asociaciones de elementos que puede configurarse en Sí o No, y esto debería controlar si habilitamos la funcionalidad de asociación en nuestro componente. Joomla proporciona una función JLanguageAssociations::isEnabled() que devuelve true si JLanguageMultilang::isEnabled() y la opción Asociaciones de elementos está establecida en Sí , por lo que necesitamos verificar esto antes de mostrar la funcionalidad de las asociaciones en el lado servidor.

Acceso a la base de datos

Las asociaciones se almacenan en la tabla de asociaciones de Joomla, que tiene 3 campos

  • context: el tipo de registro al que se refiere esta asociación, por ejemplo, com_menus.item o com_categories.item
  • id: el id del registro al que se refiere la asociación, por ejemplo, el id del elemento del menú o id de la categoría
  • key: un campo que le permite a Joomla encontrar los otros registros que están asociados con este, porque todos tienen la misma clave. (Joomla toma una matriz de los ids + contexto de los registros que están asociados entre sí, JSON codifica esto para formar una cadena y toma un hash MD5 de la cadena para formar el valor de la clave que luego se almacena en cada uno de ellos archivos).

Joomla proporciona una función JLanguageAssociations::getAssociations que permite a los componentes encontrar registros asociados. Le pasa context e id de su registro, y luego encuentra registros en la tabla de asociaciones que tienen la misma clave que la suya, devolviendo los resultados en forma de una matriz asociativa, codificada por la etiqueta de idioma. Como veremos, también devolverá información adicional sobre los registros resultantes, como el alias y la identificación de categoría, siempre que le indiquemos dónde almacenamos esos datos, para que puedan agregar las cláusulas de SQL JOIN apropiadas. (Aunque dentro de su código hace una combinación de SQL INNER con la tabla de categorías, por lo que no encuentra asociaciones que no tengan una categoría válida).

(Ten en cuenta que vamos a tener otras 2 funciones que también se denominan getAssociations, una en nuestro archivo de ayuda de com_associations y otra en nuestro archivo helper del lado cliente. ¡Es fácil confundirse!)

El código para actualizar y eliminar los registros de asociación está en la clase JModelAdmin, que nuestro propio modelo de helloworld de administrador hereda, y para reutilizarlo solo necesitamos especificar el $associationsContext que estamos usando para nuestras asociaciones.

Visualizar Helloworlds

Agregaremos otra columna en nuestra pantalla para las asociaciones. Después de obtener las asociaciones como se describe anteriormente, utilizaremos un archivo de diseño estándar utilizado por los componentes principales de Joomla para mostrar las asociaciones. Siguiendo el ejemplo de otros componentes, pondremos este código en una función dentro de un archivo heper al que se llamará desde nuestro archivo de diseño de helloworlds, aunque con una implementación de MVC que obtenga los detalles de las asociaciones, por lo general encajaría en el ámbito del modelo.

Editar Helloworld

En línea con otros componentes de Joomla, agregamos a nuestro archivo de diseño edit.php otra pestaña bootstrap donde el administrador puede establecer las asociaciones. En esta pestaña, pondremos campos para seleccionar una asociación para cada uno de los otros idiomas de contacto en nuestro sistema, pero como no sabemos cuántos idiomas se instalarán, debemos agregar estos campos de forma dinámica (en nuestro modelo) usando Las API de JForm, en lugar de especificarlas en un archivo XML.

Cuando el administrador presione el botón de selección para seleccionar el registro asociado, mostraremos un modal que muestra los registros de hellowworld, tal como lo hicimos en el paso del tutorial anterior cuando definimos un artículo, y reutilizaremos el código de definición del campo modal que desarrollamos ahí. Sin embargo, en este caso, cuando se muestra el modal, queremos restringir los registros que se muestran solo a aquellos con el idioma correcto, y para habilitarlo usamos un parámetro adicional forcedLanguage, que está establecido en la etiqueta del contenido del lenguaje en cuestión.

Como es habitual, al editar registros debemos rellenar previamente el formulario con cualquier asociación existente.

Finalmente, en nuestro modelo, definimos el $associationsContext para que el código de JModelAdmin pueda manejar las actualizaciones de la base de datos de los registros de asociaciones.

Habilitar el Componente Asociaciones Multilingües

En primer lugar, debemos proporcionar algunos datos de configuración (como se describe en Asociaciones Multilingüe - Desarrolladores) para habilitar el componente Asociaciones Multilingüe (com_associations) para que funcione con nuestro componente helloworld . También debemos definir un par de cadenas de idioma para el cuadro de selección desplegable para elegir con qué tipos de registro trabajar.

Una vez que el usuario selecciona el tipo de registro, los registros de referencia y de destino se pueden mostrar uno al lado del otro en 2 iframes. En nuestro caso, la URL de los iframes será similar a cuando el administrador haga clic en un registro en la vista de helloworlds, es decir, que pase el ID del registro con task=helloworld.edit. Entonces reutilizaremos nuestro código MVC para editar un registro de hellowworld, pero serán necesarios dos cambios para generar la visualización que necesitamos.

En primer lugar, como estos formularios de edición estarán en un iframe, no queremos que se muestren los encabezados del menú de Joomla, por lo que adoptaremos el mismo enfoque que en el paso anterior cuando agregamos un modal, y usaremos el parámetro &tmpl=component de la URL.

Además, com_associations no permite que el usuario cambie el idioma o las asociaciones dentro de los iframes, e implementa eso ocultando nuestros campos y mostrando sus propios campos que muestran solo el valor actual, y para mostrar estos campos correctamente debemos incluir la biblioteca Chosen utilizando JHtml::_('formbehavior.chosen', 'select').

Enlace a los registros asociados en el lado cliente

Cuando Joomla está generando los enlaces hreflang e idioma para nuestro componente helloworld en el lado cliente, busca una clase HelloworldHelperAssociation en un archivo helper association.php e intenta llamar al método getAssociations de esa clase, y esto es lo que tenemos que codificar. Joomla pasa la vista y el ID de registro, y devolvemos el ID del registro o categoría de helloworld asociado.

Habilitar Asociaciones para las categorías de helloworld

Joomla viene con todo el código para soportar asociaciones para nuestras categorías de helloworld. Verifica si el archivo helper del lado cliente association.php está presente, con la clase correcta HelloworldHelperAssociation, y si es así, habilita la funcionalidad para establecer asociaciones de categorías en el lado servidor y usarlas en la Interfaz.

Otros cambios resultantes

Ahora estamos usando nuestra vista de Helloworlds en 3 contextos diferentes

  1. Mostrar una tabla de nuestros registros de helloworld cuando el administrador hace clic en Componentes/Hola mundo: funcionalidad desarrollada hace algún tiempo
  2. Mostrar los registros de helloworld en una modal cuando el administrador está definiendo un menú del sitio que apunta a un registro de helloworld, y debe elegir el registro que se va a mostrar: funcionalidad desarrollada en el paso anterior, Agregar una modal
  3. Mostrar los registros específicos del idioma en un modo para que el administrador pueda seleccionar la función de registro de helloworld asociada en este paso.

Con cada uno de estos estamos mostrando los campos filtro/búsqueda en la parte superior del formulario, y como se describe en el paso Agregar DEcoraciones del lado servidor, siempre que el administrador selecciona, por ejemplo, un filtro en uno de estos campos, la información se almacena en la sesión del usuario, de modo que cuando se vuelve a mostrar el formulario se aplica el mismo filtro. El problema es que actualmente esta información se almacena en el mismo lugar para los 3 contextos, por lo que, por ejemplo, si el administrador elige un registro en francés para una asociación y luego hace clic en Componentes/Hola mundo, entonces solo mostrará los registros en francés de la tabla.

Para solucionar esto en nuestro modelo de helloworlds, necesitamos determinar qué contexto es y configurar la variable $context de manera diferente, ya que esta variable protegida por JModelAdmin es la clave para la información de esta sesión. (Ten en cuenta que este contexto no tiene nada que ver con el campo de contexto en la tabla de asociaciones).

Además, deberemos realizar algunos cambios en nuestro enrutador personalizado de helloworld.

Administración del MVC Helloworlds

Para la pantalla de helloworlds de nuestro administrador, no se requieren cambios en el controlador, pero los otros archivos de origen deben actualizarse.

En el modelo de helloworlds los cambios son los siguientes:

  • Agregando asociaciones en la consulta SQL. En el modelo solo determinamos si un registro tiene una asociación en la tabla de asociaciones, y obtenemos los registros asociados reales en el archivo helper más abajo.
  • Agregar "asociación" a la matriz de filter_fields, que es la lista de columnas para las que se permite la clasificación.
  • Configurar la variable $context para separar la información de la sesión del usuario para los diferentes contextos en los que se usa la visualización de helloworlds.

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',
				'association',
				'published'
			);
		}

		parent::__construct($config);
	}

	protected function populateState($ordering = null, $direction = null)
	{
		$app = JFactory::getApplication();

		// Adjust the context to support modal layouts.
		if ($layout = $app->input->get('layout'))
		{
			$this->context .= '.' . $layout;
		}

		// Adjust the context to support forced languages.
		$forcedLanguage = $app->input->get('forcedLanguage', '', 'CMD');
		if ($forcedLanguage)
		{
			$this->context .= '.' . $forcedLanguage;
		}

		parent::populateState($ordering, $direction);
        
		// If there's a forced language then define that filter for the query where clause
		if (!empty($forcedLanguage))
		{
			$this->setState('filter.language', $forcedLanguage);
		}
	}

	/**
	 * 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');

		// Join over the associations - we just want to know if there are any, at this stage
		if (JLanguageAssociations::isEnabled())
		{
			$query->select('COUNT(asso2.id)>1 as association')
				->join('LEFT', '#__associations AS asso ON asso.id = a.id AND asso.context=' . $db->quote('com_helloworld.item'))
				->join('LEFT', '#__associations AS asso2 ON asso2.key = asso.key')
				->group('a.id');
		}

		// 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;
	}
}

También tenemos que cambiar la definición del campo de ordenamiento dentro del archivo de definición XML de campos de filtro para permitir que el ordenamiento se realice por la asociación.

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"
			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>
			<option value="association ASC">COM_HELLOWORLD_ASSOCIATION_ASC</option>
			<option value="association DESC">COM_HELLOWORLD_ASSOCIATION_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>

Si estamos mostrando la vista de helloworlds para que el administrador pueda seleccionar un registro para una asociación, entonces estamos mostrando registros de un determinado idioma y no queremos permitir que el administrador cambie el idioma mediante el uso del campos de filtro. Los campos de filtro se definen a partir del archivo xml anterior, así que usamos la API de JForm para cambiar dinámicamente el campo language en el formulario para que esté oculto.

La propiedad activeFilters de la vista se puede usar en el diseño de searchtools, por lo que desarmamos el elemento de idioma.

También eliminamos la línea que previamente estableció la variable $context.

admin/views/helloworlds/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');

/**
 * HelloWorlds View
 *
 * @since  0.0.1
 */
class HelloWorldViewHelloWorlds extends JViewLegacy
{
	/**
	 * Display the Hello World view
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
	 *
	 * @return  void
	 */
	function display($tpl = null)
	{
		// Get application
		$app = JFactory::getApplication();

		// Get data from the model
		$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');
        
		// What Access Permissions does this user have? What can (s)he do?
		$this->canDo = JHelperContent::getActions('com_helloworld');

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			JError::raiseError(500, implode('<br />', $errors));

			return false;
		}
        
		// Set the sidebar submenu and toolbar, but not on the modal window
		if ($this->getLayout() !== 'modal')
		{
			HelloWorldHelper::addSubmenu('helloworlds');
			$this->addToolBar();
		}
		else
		{
			// If it's being displayed to select a record as an association, then forcedLanguage is set
			if ($forcedLanguage = $app->input->get('forcedLanguage', '', 'CMD'))
			{
				// Transform the language selector filter into an hidden field, so it can't be set
				$languageXml = new SimpleXMLElement('<field name="language" type="hidden" default="' . $forcedLanguage . '" />');
				$this->filterForm->setField($languageXml, 'filter', true);

				// Also, unset the active language filter so the search tools is not open by default with this filter.
				unset($this->activeFilters['language']);
			}
		}

		// Display the template
		parent::display($tpl);

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

	/**
	 * Add the page title and toolbar.
	 *
	 * @return  void
	 *
	 * @since   1.6
	 */
	protected function addToolBar()
	{
		$title = JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLDS');

		if ($this->pagination->total)
		{
			$title .= "<span style='font-size: 0.5em; vertical-align: middle;'>(" . $this->pagination->total . ")</span>";
		}

		JToolBarHelper::title($title, 'helloworld');
		if ($this->canDo->get('core.create')) 
		{
			JToolBarHelper::addNew('helloworld.add', 'JTOOLBAR_NEW');
		}
		if ($this->canDo->get('core.edit')) 
		{
			JToolBarHelper::editList('helloworld.edit', 'JTOOLBAR_EDIT');
		}
		if ($this->canDo->get('core.delete')) 
		{
			JToolBarHelper::deleteList('', 'helloworlds.delete', 'JTOOLBAR_DELETE');
		}
		if ($this->canDo->get('core.admin')) 
		{
			JToolBarHelper::divider();
			JToolBarHelper::preferences('com_helloworld');
		}
	}
	/**
	 * Method to set up the document properties
	 *
	 * @return void
	 */
	protected function setDocument() 
	{
		$document = JFactory::getDocument();
		$document->setTitle(JText::_('COM_HELLOWORLD_ADMINISTRATION'));
	}
}

No cambiaremos nuestro archivo de diseño modal para mostrar las asociaciones, pero como este diseño se usará para seleccionar un registro de helloworld como asociación, debemos atender el parámetro forcedLanguage.

admin/views/helloworlds/tmpl/modal.php

<?php
/**
 * Layout file for the admin modal display of helloworld records
 *
 */

defined('_JEXEC') or die('Restricted Access');

use Joomla\Registry\Registry;

JHtml::_('behavior.core');
JHtml::_('script', 'com_helloworld/admin-helloworlds-modal.js', array('version' => 'auto', 'relative' => true));

$listOrder     = $this->escape($this->state->get('list.ordering'));
$listDirn      = $this->escape($this->state->get('list.direction'));

$app = JFactory::getApplication();
$function  = $app->input->getCmd('function', 'jSelectHelloworld');
$onclick   = $this->escape($function);
?>
<div class="container-popup">
    
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&view=helloworlds&layout=modal&tmpl=component&function=' . $function . '&' . JSession::getFormToken() . '=1'); ?>" method="post" name="adminForm" id="adminForm" class="form-inline">

	<?php echo JLayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?>
    
    <div class="clearfix"></div>

        <table class="table table-striped table-hover">
            <thead>
            <tr>
                <th width="3%"><?php echo JText::_('COM_HELLOWORLD_NUM'); ?></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) :
                        $row->image = new Registry;
                        $row->image->loadString($row->imageInfo);
                        if ($row->language && JLanguageMultilang::isEnabled())
                        {
                            $tag = strlen($row->language);
                            if ($tag == 5)
                            {
                                $lang = substr($row->language, 0, 2);
                            }
                            elseif ($tag == 6)
                            {
                                $lang = substr($row->language, 0, 3);
                            }
                            else {
                                $lang = '';
                            }
                        }
                        elseif (!JLanguageMultilang::isEnabled())
                        {
                            $lang = '';
                        }
                    ?>
                        <tr>
                            <td><?php echo $this->pagination->getRowOffset($i); ?></td>
                            <td>
                                <?php 
                                $link = 'index.php?option=com_helloworld&view=helloworld&id=' . $row->id;
                                $attribs = 'data-function="' . $this->escape($onclick) . '"'
								. ' data-id="' . $row->id . '"'
								. ' data-title="' . $this->escape(addslashes($row->greeting)) . '"'
								. ' data-uri="' . $link . '"'
								. ' data-language="' . $this->escape($lang) . '"'
                                ;
                                ?>
                                <a class="select-link" href="javascript:void(0)" <?php echo $attribs; ?>>
                                    <?php echo $this->escape($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"/>
        <input type="hidden" name="forcedLanguage" value="<?php echo $app->input->get('forcedLanguage', '', 'CMD'); ?>" />
        <?php echo JHtml::_('form.token'); ?>
</form>
</div>

Cambiamos nuestro diseño predeterminado para mostrar las asociaciones.

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'));
$assoc = JLanguageAssociations::isEnabled();
$authorFieldwidth = $assoc ? "10%" : "25%";
JLoader::register('JHtmlHelloworlds', JPATH_ADMINISTRATOR . '/components/com_helloworld/helpers/html/helloworlds.php');
?>
<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>
                <?php if ($assoc) : ?>
                    <th width="15%">
                        <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_HELLOWORLDS_ASSOCIATIONS', 'association', $listDirn, $listOrder); ?>
                    </th>
                <?php endif; ?>
                <th width="<?php echo $authorFieldwidth; ?>">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_AUTHOR', 'author', $listDirn, $listOrder); ?>
                </th>
                <th width="10%">
                    <?php echo JHtml::_('searchtools.sort', 'COM_HELLOWORLD_LANGUAGE', 'language', $listDirn, $listOrder); ?>
                </th>
                <th width="10%">
                    <?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>
                            <?php if ($assoc) : ?>
                                <td align="center">
                                    <?php if ($row->association) : ?>
                                        <?php echo JHtml::_('helloworlds.association', $row->id); ?>
                                    <?php endif; ?>
                                </td>
                            <?php endif; ?>
                            <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>

Siguiendo el enfoque de los componentes principales de Joomla que usan asociaciones, la visualización detallada de las asociaciones se delega a un archivo de ayuda que se coloca en un nuevo subdirectorio html en nuestro directorio de administradores de ayuda existentes. Esto luego muestra las asociaciones usando un archivo de diseño en joomla/content/associations.php, de la misma manera que lo hacen los otros componentes.

admin/helpers/html/helloworlds.php

<?php
/**
 * Helper file for outputting html associated with the helloworld administrator functionality
 */

defined('_JEXEC') or die;

JLoader::register('HelloworldHelper', JPATH_ADMINISTRATOR . '/components/com_helloworld/helpers/helloworld.php');

class JHtmlHelloworlds
{
	/**
	 * Render the list of associated items
	 *
	 * @param   integer  $id  The id of the helloworld record
	 *
	 * @return  string  The language HTML
	 *
	 * @throws  Exception
	 */
	public static function association($id)
	{
		// Defaults
		$html = '';

		// Get the associations
		if ($associations = JLanguageAssociations::getAssociations('com_helloworld', '#__helloworld', 'com_helloworld.item', (int)$id))
		{
			foreach ($associations as $tag => $associated)
			{
				$associations[$tag] = (int) $associated->id;
			}

			// get the relevant category titles and languages, for the tooltip
			$db = JFactory::getDbo();
			$query = $db->getQuery(true)
				->select('h.*')
				->select('l.sef as lang_sef')
				->select('l.lang_code')
				->from('#__helloworld as h')
				->select('cat.title as category_title')
				->join('LEFT', '#__categories as cat ON cat.id=h.catid')
				->where('h.id IN (' . implode(',', array_values($associations)) . ')')
				->join('LEFT', '#__languages as l ON h.language=l.lang_code')
				->select('l.image')
				->select('l.title as language_title');
			$db->setQuery($query);

			try
			{
				$items = $db->loadObjectList('id');
			}
			catch (RuntimeException $e)
			{
				throw new Exception($e->getMessage(), 500, $e);
			}

			if ($items)
			{
				foreach ($items as &$item)
				{
					$text    = $item->lang_sef ? strtoupper($item->lang_sef) : 'XX';
					$url     = JRoute::_('index.php?option=com_helloworld&task=helloworld.edit&id=' . (int) $item->id);

					$tooltip = htmlspecialchars($item->greeting, ENT_QUOTES, 'UTF-8') . '<br />' . JText::sprintf('JCATEGORY_SPRINTF', $item->category_title);
					$classes = 'hasPopover label label-association label-' . $item->lang_sef;

					$item->link = '<a href="' . $url . '" title="' . $item->language_title . '" class="' . $classes
						. '" data-content="' . $tooltip . '" data-placement="top">'
						. $text . '</a>';
				}
			}

			JHtml::_('bootstrap.popover');

			$html = JLayoutHelper::render('joomla.content.associations', $items);
		}

		return $html;
	}
}

Administración del MVC Helloworlds (editar)

Los controladores de helloworld no requieren cambios, pero los otros archivos sí lo requieren. Nuestro modelo tiene una serie de cambios:

  1. Necesitamos cambiar nuestra forma de edición dinámicamente para incluir los campos para capturar las asociaciones. Después de cargar nuestro formulario, Joomla llama a una función de devolución de llamada preprocessForm y es allí donde colocamos nuestro código para agregar esos campos, y usamos las API de JForm para modificar el formulario. Ten en cuenta que agregamos campos para todos los idiomas de contenido, incluido el idioma actual del registro que se está editando. Joomla incluye algo de código javascript que oculta el campo de asociación para el idioma actual, y cambia de manera inteligente los campos que se muestran si cambiamos el valor en el campo de idioma.
  2. En el caso de editar un registro existente necesitamos rellenar previamente los datos de las asociaciones. La función de devolución de llamada en este caso es loadFormData, y desde allí llamamos a getItem si necesitamos rellenar previamente los datos del formulario de la base de datos, por lo que es ahí donde debemos agregar nuestro código. Para conseguir las asociaciones.
  3. Como se mencionó anteriormente, el código JModelAdmin maneja las actualizaciones de la base de datos de los datos de la asociación, siempre que configuremos la variable protegida $associacionContext de manera adecuada.

admin/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');

use Joomla\Registry\Registry;

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelAdmin
{
	// JModelAdmin needs to know this for storing the associations 
	protected $associationsContext = 'com_helloworld.item';

	/**
	 * Method to override getItem to allow us to convert the JSON-encoded image information
	 * in the database record into an array for subsequent prefilling of the edit form
	 * We also use this method to prefill the associations
	 */
	public function getItem($pk = null)
	{
		$item = parent::getItem($pk);
		if ($item AND property_exists($item, 'image'))
		{
			$registry = new Registry($item->image);
			$item->imageinfo = $registry->toArray();
		}

		// Load associated items
		if (JLanguageAssociations::isEnabled())
		{
			$item->associations = array();

			if ($item->id != null)
			{
				$associations = JLanguageAssociations::getAssociations('com_helloworld', '#__helloworld', 'com_helloworld.item', (int)$item->id);

				foreach ($associations as $tag => $association)
				{
					$item->associations[$tag] = $association->id;
				}
			}
		}
		return $item; 
	}

	/**
	 * 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);
	}

	/**
	 * Method to get the record form.
	 *
	 * @param   array    $data      Data for the form.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  mixed    A JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	public function getForm($data = array(), $loadData = true)
	{
		// Get the form.
		$form = $this->loadForm(
			'com_helloworld.helloworld',
			'helloworld',
			array(
				'control' => 'jform',
				'load_data' => $loadData
			)
		);

		if (empty($form))
		{
			return false;
		}

		return $form;
	}

	/**
	 * Method to preprocess the form to add the association fields dynamically
	 *
	 * @return     none
	 */
	protected function preprocessForm(JForm $form, $data, $group = 'helloworld')
	{
		// Association content items
		if (JLanguageAssociations::isEnabled())
		{
			$languages = JLanguageHelper::getContentLanguages(false, true, null, 'ordering', 'asc');

			if (count($languages) > 1)
			{
				$addform = new SimpleXMLElement('<form />');
				$fields = $addform->addChild('fields');
				$fields->addAttribute('name', 'associations');
				$fieldset = $fields->addChild('fieldset');
				$fieldset->addAttribute('name', 'item_associations');

				foreach ($languages as $language)
				{
					$field = $fieldset->addChild('field');
					$field->addAttribute('name', $language->lang_code);
					$field->addAttribute('type', 'modal_helloworld');
					$field->addAttribute('language', $language->lang_code);
					$field->addAttribute('label', $language->title);
					$field->addAttribute('translate_label', 'false');
				}

				$form->load($addform, false);
			}
		}
		parent::preprocessForm($form, $data, $group);
	}

	/**
	 * Method to get the script to be included on the form
	 *
	 * @return string	Script files
	 */
	public function getScript() 
	{
		return 'administrator/components/com_helloworld/models/forms/helloworld.js';
	}

	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @return  mixed  The data for the form.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = JFactory::getApplication()->getUserState(
			'com_helloworld.edit.helloworld.data',
			array()
		);

		if (empty($data))
		{
			$data = $this->getItem();
		}

		return $data;
	}
	/**
	 * Method to override the JModelAdmin save() function to handle Save as Copy correctly
	 *
	 * @param   The helloworld record data submitted from the form.
	 *
	 * @return  parent::save() return value
	 */
	public function save($data)
	{
		$input = JFactory::getApplication()->input;

		JLoader::register('CategoriesHelper', JPATH_ADMINISTRATOR . '/components/com_categories/helpers/categories.php');

		// Validate the category id
		// validateCategoryId() returns 0 if the catid can't be found
		if ((int) $data['catid'] > 0)
		{
			$data['catid'] = CategoriesHelper::validateCategoryId($data['catid'], 'com_helloworld');
		}

		// Alter the greeting and alias for save as copy
		if ($input->get('task') == 'save2copy')
		{
			$origTable = clone $this->getTable();
			$origTable->load($input->getInt('id'));

			if ($data['greeting'] == $origTable->greeting)
			{
				list($greeting, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['greeting']);
				$data['greeting'] = $greeting;
				$data['alias'] = $alias;
			}
			else
			{
				if ($data['alias'] == $origTable->alias)
				{
					$data['alias'] = '';
				}
			}
			// standard Joomla practice is to set the new record as unpublished
			$data['published'] = 0;
		}

		return parent::save($data);
	}
	/**
	 * Method to check if it's OK to delete a message. Overrides JModelAdmin::canDelete
	 */
	protected function canDelete($record)
	{
		if( !empty( $record->id ) )
		{
			return JFactory::getUser()->authorise( "core.delete", "com_helloworld.helloworld." . $record->id );
		}
	}
}

En el archivo modelo anterior, hemos agregado a nuestro formulario de edición de helloworld un elemento XML que contiene varias etiquetas field>, una para cada uno de los campos de entrada modal_helloworld para la captura una asociación. Anteriormente definimos este campo modal_helloworld para capturar en una modal un registro de helloworld para un menú de helloworld. Al comparar el código anterior con la definición XML del formulario de menuitem (en el directorio de diseño de vista de helloworld del sitio), la diferencia clave es que hay un atributo adicional language arriba. En nuestro archivo php que define este campo modal_helloworld, usamos la presencia de este parámetro para establecer el parámetro forcedLanguage, de modo que el modal muestre registros solo de este idioma.

admin/models/fields/modal/helloworld.php

<?php

defined('JPATH_BASE') or die;

/**
 * Supports a modal for selecting a helloworld record
 *
 */
class JFormFieldModal_Helloworld extends JFormField
{
	/**
	 * Method to get the html for the input field.
	 *
	 * @return  string  The field input html.
	 */
	protected function getInput()
	{
		// Load language
		JFactory::getLanguage()->load('com_helloworld', JPATH_ADMINISTRATOR);

		// $this->value is set if there's a default id specified in the xml file
		$value = (int) $this->value > 0 ? (int) $this->value : '';
        
		// $this->id will be jform_request_xxx where xxx is the name of the field in the xml file
		// or jform_associations_xx_yy where xx_yy is the language code (hyphen replaced by underscore) for associations
		$modalId = 'Helloworld_' . $this->id;

		// Add the modal field script to the document head.
		JHtml::_('jquery.framework');
		JHtml::_('script', 'system/modal-fields.js', array('version' => 'auto', 'relative' => true));

		// our callback function from the modal to the main window:
		JFactory::getDocument()->addScriptDeclaration("
			function jSelectHelloworld_" . $this->id . "(id, title, catid, object, url, language) {
				window.processModalSelect('Helloworld', '" . $this->id . "', id, title, catid, object, url, language);
			}
			");

		// if a default id is set, then get the corresponding greeting to display it
		if ($value)
		{
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true)
				->select($db->quoteName('greeting'))
				->from($db->quoteName('#__helloworld'))
				->where($db->quoteName('id') . ' = ' . (int) $value);
			$db->setQuery($query);

			try
			{
				$title = $db->loadResult();
			}
			catch (RuntimeException $e)
			{
				JError::raiseWarning(500, $e->getMessage());
			}
		}
        
		// display the default greeting or "Select" if no default specified
		$title = empty($title) ? JText::_('COM_HELLOWORLD_MENUITEM_SELECT_HELLOWORLD') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
		$html  = '<span class="input-append">';
		$html .= '<input class="input-medium" id="' . $this->id . '_name" type="text" value="' . $title . '" disabled="disabled" size="35" />';

		// html for the Select button
		$html .= '<a'
			. ' class="btn hasTooltip' . ($value ? ' hidden' : '') . '"'
			. ' id="' . $this->id . '_select"'
			. ' data-toggle="modal"'
			. ' role="button"'
			. ' href="#ModalSelect' . $modalId . '"'
			. ' title="' . JHtml::tooltipText('COM_HELLOWORLD_MENUITEM_SELECT_BUTTON_TOOLTIP') . '">'
			. '<span class="icon-file" aria-hidden="true"></span> ' . JText::_('JSELECT')
			. '</a>';

		// html for the Clear button
		$html .= '<a'
			. ' class="btn' . ($value ? '' : ' hidden') . '"'
			. ' id="' . $this->id . '_clear"'
			. ' href="#"'
			. ' onclick="window.processModalParent(\'' . $this->id . '\'); return false;">'
			. '<span class="icon-remove" aria-hidden="true"></span>' . JText::_('JCLEAR')
			. '</a>';

		$html .= '</span>';

		// url for the iframe
		$linkHelloworlds = 'index.php?option=com_helloworld&amp;view=helloworlds&amp;layout=modal&amp;tmpl=component&amp;' . JSession::getFormToken() . '=1';
		$urlSelect = $linkHelloworlds . '&amp;function=jSelectHelloworld_' . $this->id;
        
		// title to go in the modal header
		$modalTitle    = JText::_('COM_HELLOWORLD_MENUITEM_SELECT_MODAL_TITLE');

		// if the form definition has a 'language' field then it's for the association
		// add the forcedLanguage parameter to the URL, and add the language to the modal title
		if (isset($this->element['language']))
		{
			$urlSelect .= '&amp;forcedLanguage=' . $this->element['language'];
			$modalTitle .= ' &#8212; ' . $this->element['label'];
		}

		// html to set up the modal iframe
		$html .= JHtml::_(
			'bootstrap.renderModal',
			'ModalSelect' . $modalId,
			array(
				'title'       => $modalTitle,
				'url'         => $urlSelect,
				'height'      => '400px',
				'width'       => '800px',
				'bodyHeight'  => '70',
				'modalWidth'  => '80',
				'footer'      => '<a role="button" class="btn" data-dismiss="modal" aria-hidden="true">' . JText::_('JLIB_HTML_BEHAVIOR_CLOSE') . '</a>',
			)
		);

		// class='required' for client side validation.
		$class = $this->required ? ' class="required modal-value"' : '';

		// hidden input field to store the helloworld record id
		$html .= '<input type="hidden" id="' . $this->id . '_id" ' . $class 
			. ' data-required="' . (int) $this->required . '" name="' . $this->name
			. '" data-text="' . htmlspecialchars(JText::_('COM_HELLOWORLD_MENUITEM_SELECT_HELLOWORLD', true), ENT_COMPAT, 'UTF-8') 
			. '" value="' . $value . '" />';

		return $html;
	}

	/**
	 * Method to get the html for the label field.
	 *
	 * @return  string  The field label html.
	 */
	protected function getLabel()
	{
		return str_replace($this->id, $this->id . '_id', parent::getLabel());
	}
}

El archivo de vista no requiere modificación, pero el archivo de diseño edit.php tiene los siguientes cambios

  1. Incluye la pestaña Asociaciones para permitir que el administrador las edite. Como se mencionó anteriormente, esto se debe mostrar solo si las asociaciones en general están habilitadas a través del complemento de filtro de idioma del sistema.
  2. Si la página se inicia desde el componente com_associations, se habrá configurado &tmpl=component para evitar mostrar los menús de Joomla. En este caso, se debe agregar el mismo parámetro a la url de destino del formulario.
  3. Incluye la biblioteca chosen para que los campos generados por com_associations se muestren correctamente.

La visualización de los campos de asociaciones se maneja mediante un archivo de diseño común joomla/edit/associaciones , y es desde aquí que se incluye el código javascript system/associations-edit.js (que oculta el campo de asociación para el idioma actual, etc., como se mencionó anteriormente).

admin/views/helloworld/tmpl/edit.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
defined('_JEXEC') or die('Restricted access');
JHtml::_('behavior.formvalidator');

// The following is to enable setting the permission's Calculated Setting 
// when you change the permission's Setting. 
// The core javascript code for initiating the Ajax request looks for a field
// with id="jform_title" and sets its value as the 'title' parameter to send in the Ajax request
JFactory::getDocument()->addScriptDeclaration('
	jQuery(document).ready(function() {
        greeting = jQuery("#jform_greeting").val();
		jQuery("#jform_title").val(greeting);
	});
');

// Required for proper display of fields generated by com_associations
JHtml::_('formbehavior.chosen', 'select');

// if &tmpl=component used on first invocation, ensure it's on subsequent ones too
$input = JFactory::getApplication()->input;
$tmpl = $input->getCmd('tmpl', '') === 'component' ? '&tmpl=component' : '';
?>
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&layout=edit' . $tmpl . '&id=' . (int) $this->item->id); ?>"
    method="post" name="adminForm" id="adminForm" class="form-validate">
    
    <input id="jform_title" type="hidden" name="helloworld-message-title"/>
    
    <div class="form-horizontal">

    <?php echo JHtml::_('bootstrap.startTabSet', 'myTab', array('active' => 'details')); ?>
    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'details', 
        empty($this->item->id) ? JText::_('COM_HELLOWORLD_TAB_NEW_MESSAGE') : JText::_('COM_HELLOWORLD_TAB_EDIT_MESSAGE')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_DETAILS') ?></legend>
            <div class="row-fluid">
                <div class="span6">
                    <?php echo $this->form->renderFieldset('details');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>

    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'image', JText::_('COM_HELLOWORLD_TAB_IMAGE')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_IMAGE') ?></legend>
            <div class="row-fluid">
                <div class="span6">
                    <?php echo $this->form->renderFieldset('image-info');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>

    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'params', JText::_('COM_HELLOWORLD_TAB_PARAMS')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_PARAMS') ?></legend>
            <div class="row-fluid">
                <div class="span6">
                    <?php echo $this->form->renderFieldset('params');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>

    <?php if (JLanguageAssociations::isEnabled()) : ?>
        <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'associations', JText::_('COM_HELLOWORLD_TAB_ASSOCIATIONS')); ?>
            <fieldset class="adminform">
                <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_ASSOCIATIONS') ?></legend>
                <div class="row-fluid">
                    <div class="span12">
                        <?php echo JLayoutHelper::render('joomla.edit.associations', $this);  ?>
                    </div>
                </div>
            </fieldset>
        <?php echo JHtml::_('bootstrap.endTab'); ?>
    <?php endif; ?>

    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'permissions', JText::_('COM_HELLOWORLD_TAB_PERMISSIONS')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_PERMISSIONS') ?></legend>
            <div class="row-fluid">
                <div class="span12">
                    <?php echo $this->form->renderFieldset('accesscontrol');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>
    <?php echo JHtml::_('bootstrap.endTabSet'); ?>

    </div>
    <input type="hidden" name="task" value="helloworld.edit" />
    <?php echo JHtml::_('form.token'); ?>
</form>

Habilitar com_associations

Para habilitar el componente Asociaciones multilingües, como se describe aquí debemos incluir el archivo helper a continuación, y agregar 2 cadenas al archivo de idioma del sistema, así como otros aspectos de formato ya cubiertos anteriormente.

El archivo de ayuda debe tener los arrays $fields y $support definidos porque com_associations realiza algunos aspectos en nuestro nombre, como unirse a nuestro helloworld para proporcionar información adicional, y también actualizar registros, por lo que debe saber cómo SQL JOIN en nuestra tabla de base de datos y qué capacidades admitimos con respecto al control de acceso y el bloqueo. La función getFieldsTemplate() configura las valules predeterminadas en la matriz $fields, por ejemplo, especificando que el alias se encontrará en un campo de la base de datos llamado 'alias', y una identificación de categoría en el campo llamado 'catid', por lo que realmente necesitamos especificar solo las excepciones, como encontrar el título en el campo de la base de datos 'saludo'. Y de manera similar, getSupportTemplate() para la matriz $ support.

(Si en tu componente desea admitir la configuración de ACL en un registro de helloworld individual o nivel de categoría, entonces necesitarás una función adicional en el siguiente archivo: public function allowEdit($typeName, $recordId), similar a lo que tendrías en un controlador, pero esto está realmente fuera del alcance de este tutorial).

admin/helpers/associations.php

<?php
/**
 * The Helloworld helper file for Multilingual Associations
 */

defined('_JEXEC') or die;

JTable::addIncludePath(__DIR__ . '/../tables');

class HelloworldAssociationsHelper extends JAssociationExtensionHelper
{
	/**
	 * The extension name
	 */
	protected $extension = 'com_helloworld';

	/**
	 * Array of item types which have associations
	 */
	protected $itemTypes = array('helloworld', 'category');

	/**
	 * Has the extension association support
	 */
	protected $associationsSupport = true;

	/**
	 * Get the associated items for an item
	 *
	 * @param   string  $typeName  The item type, either 'helloworld' or 'category'
	 * @param   int     $id        The id of item for which we need the associated items
	 *
	 */
	public function getAssociations($typeName, $id)
	{
		$type = $this->getType($typeName);

		$context    = $this->extension . '.item';
		$catidField = 'catid';

		if ($typeName === 'helloworld')
		{
			$context    = 'com_helloworld.item';
			$catidField = 'catid';
		}
        elseif ($typeName === 'category')
		{
			$context    = 'com_categories.item';
			$catidField = '';
		}
        else
        {
            return null;
        }

		// Get the associations.
		$associations = JLanguageAssociations::getAssociations(
			$this->extension,
			$type['tables']['a'],
			$context,
			$id,
			'id',
			'alias',
			$catidField
		);

		return $associations;
	}

	/**
	 * Get item information
	 *
	 * @param   string  $typeName  The item type
	 * @param   int     $id        The id of item for which we need the associated items
	 *
	 * @return  JTable object associated with the record id passed in
	 */
	public function getItem($typeName, $id)
	{
		if (empty($id))
		{
			return null;
		}

		$table = null;

		switch ($typeName)
		{
			case 'helloworld':
				$table = JTable::getInstance('Helloworld', 'HelloworldTable');
				break;

			case 'category':
				$table = JTable::getInstance('Category');
				break;
		}

		if (empty($table))
		{
			return null;
		}

		$table->load($id);

		return $table;
	}

	/**
	 * Get information about the type
	 *
	 * @param   string  $typeName  The item type
	 *
	 * @return  array  Array of item types
	 */
	public function getType($typeName = '')
	{
		$fields  = $this->getFieldsTemplate();
		$tables  = array();
		$joins   = array();
		$support = $this->getSupportTemplate();
		$title   = '';

		if (in_array($typeName, $this->itemTypes))
		{
			switch ($typeName)
			{
				case 'helloworld':
					$fields['title'] = 'a.greeting';
                    $fields['ordering'] = '';
                    $fields['access'] = '';
                    $fields['state'] = 'a.published';
                    $fields['created_user_id'] = '';
                    $fields['checked_out'] = '';
                    $fields['checked_out_time'] = '';

					$support['state'] = true;
					$support['acl'] = false;
					$support['category'] = true;

					$tables = array(
						'a' => '#__helloworld'
					);

					$title = 'helloworld';
					break;

				case 'category':
					$fields['created_user_id'] = 'a.created_user_id';
					$fields['ordering'] = 'a.lft';
					$fields['level'] = 'a.level';
					$fields['catid'] = '';
					$fields['state'] = 'a.published';

					$support['state'] = true;
					$support['acl'] = true;
					$support['checkout'] = true;
					$support['level'] = true;

					$tables = array(
						'a' => '#__categories'
					);

					$title = 'category';
					break;
			}
		}

		return array(
			'fields'  => $fields,
			'support' => $support,
			'tables'  => $tables,
			'joins'   => $joins,
			'title'   => $title
		);
	}
}

admin/language/en-GB/en-GB.com_helloworld.sys.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="Hello World!"
COM_HELLOWORLD_DESCRIPTION="This is the Hello World description"
COM_HELLOWORLD_HELLOWORLD_VIEW_DEFAULT_TITLE="Hello World"
COM_HELLOWORLD_HELLOWORLD_VIEW_DEFAULT_DESC="This view displays a selected message"
COM_HELLOWORLD_ADD_VIEW_TITLE="Hello World form"
COM_HELLOWORLD_ADD_VIEW_DESC="This displays a form to allow the user to enter a new message"
COM_HELLOWORLD_CATEGORY_VIEW_TITLE="Category List"
COM_HELLOWORLD_CATEGORY_VIEW_DESC="This displays the messages in the selected category"
COM_HELLOWORLD_INSTALL_TEXT="HelloWorld Install script"
COM_HELLOWORLD_MENU="Hello World!"
COM_HELLOWORLD_POSTFLIGHT_DISCOVER_INSTALL_TEXT="HelloWorld postflight discover install script"
COM_HELLOWORLD_POSTFLIGHT_INSTALL_TEXT="HelloWorld postflight install script"
COM_HELLOWORLD_POSTFLIGHT_UNINSTALL_TEXT="HelloWorld postflight uninstall script"
COM_HELLOWORLD_POSTFLIGHT_UPDATE_TEXT="HelloWorld postflight update script"
COM_HELLOWORLD_PREFLIGHT_DISCOVER_INSTALL_TEXT="HelloWorld preflight discover install script"
COM_HELLOWORLD_PREFLIGHT_INSTALL_TEXT="HelloWorld preflight install script"
COM_HELLOWORLD_PREFLIGHT_UNINSTALL_TEXT="HelloWorld preflight uninstall script"
COM_HELLOWORLD_PREFLIGHT_UPDATE_TEXT="HelloWorld preflight update script"
COM_HELLOWORLD_UNINSTALL_TEXT="HelloWorld Uninstall script"
COM_HELLOWORLD_UPDATE_TEXT="HelloWorld Update script. HelloWorld now updated to version %s."
; Using the Multilingual Associations component
COM_HELLOWORLD_HELLOWORLDS="Greetings"
COM_HELLOWORLD_CATEGORIES="Categories"

Lado Cliente

El código de asociaciones del lado cliente es bastante estándar en todos los componentes de Joomla y comprende el archivo auxiliar a continuación. Joomla llama a nuestro método getAssociations con las id y view bajo consideración, y debe devolver información sobre los registros asociados, en particular el idioma y la URL de cada. Si la view es 'helloworld', la id se relaciona con la identificación del registro de helloworld. Si la view es 'categoría', la id se relaciona con el id de la categoría de helloworld.

La presencia de este archivo permite la funcionalidad de asociaciones de com_categories en el lado servidor, lo que permite al administrador configurar asociaciones en las categorías de helloworld.

site/helpers/association.php

<?php
/**
 * Helper file for Helloworld Associations (on the site part)
 */

defined('_JEXEC') or die;

JLoader::register('CategoryHelperAssociation', JPATH_ADMINISTRATOR . '/components/com_categories/helpers/association.php');

/**
 * Helloworld Component Association Helper
 *
 */
abstract class HelloworldHelperAssociation extends CategoryHelperAssociation
{
	/**
	 * Method to get the associations for a given item
	 *
	 * @param   integer  $id    Id of the item (helloworld id or catid, depending on view)
	 * @param   string   $view  Name of the view ('helloworld' or 'category')
	 *
	 * @return  array   Array of associations for the item
	 */
	public static function getAssociations($id = 0, $view = null)
	{
		$input = JFactory::getApplication()->input;
		$view = $view === null ? $input->get('view') : $view;
		$id = empty($id) ? $input->getInt('id') : $id;

		if ($view === 'helloworld')
		{
			if ($id)
			{
				$associations = JLanguageAssociations::getAssociations('com_helloworld', '#__helloworld', 'com_helloworld.item', $id);

				$return = array();

				foreach ($associations as $tag => $item)
				{
					$link = 'index.php?option=com_helloworld&view=helloworld&id=' . $item->id . '&catid=' . $item->catid;
					if ($item->language && $item->language !== '*' && JLanguageMultilang::isEnabled())
					{
						$link .= '&lang=' . $item->language;
					}
					$return[$tag] = $link;
				}

				return $return;
			}
		}

		if ($view === 'category' || $view === 'categories')
		{
			return self::getCategoryAssociations($id, 'com_helloworld');
		}

		return array();
	}
}

Enrutador personalizado

Hay varios cambios requeridos a nuestro código del enrutador personalizado. Anteriormente tratábamos con URL que estaban relacionadas con el mismo idioma. Ahora, en nuestro conmutador de idioma y enlaces hreflang a registros asociados, tenemos que admitir URL que también se relacionan con otros idiomas.

En primer lugar, ten en cuenta que al proporcionar parámetros para las URL asociadas en nuestro archivo del sitio helper de asociación

  • hemos incluido un parámetro lang para indicar el idioma del registro asociado
  • no hemos incluido ningún parámetro Itemid para indicar el artículo de destino.

Por lo tanto, estas URL se formarán utilizando el Itemid del menuitem en el que se encuentra actualmente el usuario del sitio, y ese menuitem está obviamente relacionado con el idioma actual. Sin embargo, queremos apuntar a un artículo del idioma apropiado del registro asociado, lo que significa cambiar el 'Itemid' dentro de la matriz $query. Lo hacemos en la función preprocess() del enrutador personalizado, a la que se llama antes de build() para cada enlace relacionado con helloworld en la página. La estrategia que adoptaremos es que lo haremos.

  • comprobar si los parámetros de URL lang e Itemid coinciden desde la perspectiva del idioma; en este caso, no tenemos que hacer nada, de lo contrario
  • ver si el menuitem actual tiene un menuitem asociado en el idioma de destino, y si es así, usarlo
  • si no, utilizar la página de inicio del idioma de destino.

(La función preprocess() se llama varias veces para cada página web, por lo que usaremos una variable estática para acelerar la comprobación en el caso normal, es decir, la URL no se relaciona con una asociación).

En segundo lugar, en nuestra función build() tenemos que eliminar la comprobación de que la URL de destino está en el mismo idioma que la página actual.

El tercer problema se relaciona con tener que construir la URL en la parte superior de la página de inicio del idioma de destino. Por defecto, la página de inicio contiene artículos destacados, por lo que su componente relacionado es com_content. Lo estamos usando para mostrar una vista com_helloworld, por lo que estamos anulando el componente, y cuando lo hacemos obtenemos una URL del formulario .../component/helloworld /... Esto no es en sí mismo un problema , pero un efecto secundario de esto es que en esta página no obtenemos un artículo activo, por lo que debemos estar preparados para manejar eso, particularmente en nuestra función parse(), eliminando la verificación de que no está establecido

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->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 or have /component type of url
			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 (isset($activeMenuitem) && $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)
	{
		static $currentLang = null;
		if (JLanguageAssociations::isEnabled())
		{
			$app  = JFactory::getApplication();
			$sitemenu = $app->getMenu();
			$lang = $query['lang'];

			if (!isset($query['lang']) || ($query['lang'] == $currentLang))
			{
				return $query;
			}

			if (!isset($query['Itemid']))  // we're currently on /component type of URL
			{
				// use the home page for the URL's language
				$home = $sitemenu->getItems(array('language','home'), array($lang, true));
				if ($home)
				{
					$query['Itemid'] = $home[0]->id;
				}
				return $query;
			}

			$itemid = $query['Itemid'];

			// ensure the menuitem for Itemid has the correct language
			$thismenuitem = $sitemenu->getItem($itemid);
			$thismenuitemLang = $thismenuitem->language;
			if ($thismenuitemLang == $lang)
			{
				$currentLang = $thismenuitemLang;
			}
			if ($thismenuitemLang == $lang || $thismenuitemLang == '*')
			{
				return $query;
			}
            
			// if not, try to find an associated menuitem with the correct language
			$associations = JLanguageAssociations::getAssociations('com_menus', '#__menu', 'com_menus.item', $itemid, 'id', '', '');
			if (isset($associations[$lang]))
			{
				$query['Itemid'] = (int) $associations[$lang]->id;
				return $query;
			}
			else // use the home page for that language (if it's set)
			{
				$home = $sitemenu->getItems(array('language','home'), array($lang, true));
				if ($home)
				{
					$query['Itemid'] = $home[0]->id;
				}
			}
		}
		return $query;
	}
}

Actualización cadenas de idioma

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_HELLOWORLDS_ASSOCIATIONS="Associations"
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_ASSOCIATIONS="Associations"
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_ASSOCIATIONS="Message Associations"
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"
COM_HELLOWORLD_ASSOCIATION_ASC="Association ascending"
COM_HELLOWORLD_ASSOCIATION_DESC="Association descending"
; Helloworld menuitem - selecting a greeting via modal
COM_HELLOWORLD_MENUITEM_SELECT_MODAL_TITLE="Select greeting"
COM_HELLOWORLD_MENUITEM_SELECT_HELLOWORLD="Select"
COM_HELLOWORLD_MENUITEM_SELECT_BUTTON_TOOLTIP="Select a helloworld greeting"

Empaquetado del componente

Contenido de su directorio de código. Cada enlace a un archivo a continuación te lleva al paso en el tutorial que tiene la última versión de ese archivo de código fuente.

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