Difference between revisions of "Making single installation packages for Joomla! 1.5 and 2.5 series"

From Joomla! Documentation

(Started working on the page; currently in progress!)
 
(Added more information; still work in progress)
Line 58: Line 58:
 
</source>
 
</source>
  
That's how easy it is! Bonus tip: take a look at how the translation keys are named in this example. It's advisable -albeit not required- to follow a hierarchical approach to your translation keys. It might sound like OCD, but it helps your translators by providing context and you to debug language related issues.
+
That's how easy it is!
 +
 
 +
== Dealing with languages ==
 +
 
 +
=== How to load component-local translation files in Joomla! 1.5 ===
 +
 
 +
Joomla! 1.6 introduced a new feature where a user can place language files inside the component's directory, e.g. administrator/com_foobar/language/en-GB/en-GB.com_foobar.ini, in order to provide language overrides. But Joomla! 1.5 doesn't support this feature. Or does it? Even though it can not do that automatically, you can do so in your extension's dispatcher file, e.g. administrator/com_foobar/foobar.php. Here's the magic code to do that in the backend:
 +
<source lang="php">
 +
$jlang =& JFactory::getLanguage();
 +
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, null, true);
 +
$jlang->load('com_foobar', JPATH_COMPONENT_ADMINISTRATOR, null, true);
 +
</source>
 +
and the frontend:
 +
<source lang="php">
 +
$jlang =& JFactory::getLanguage();
 +
$jlang->load('com_foobar', JPATH_SITE, null, true);
 +
$jlang->load('com_foobar', JPATH_COMPONENT, null, true);
 +
</source>
 +
 
 +
=== Tips and tricks for language strings ===
 +
 
 +
All your translation keys should consist of uppercase characters without spaces (that is A-Z, 0-9 and underscores). All translation values should be included in double quotation marks. This allows the translation files to be parsed by Joomla! 1.5/1.6/1.7 alike. You are also suggested to prefix your translation keys with the extension's name, albeit this is not required. For example, this is a good translation string:
 +
COM_FOOBAR_CPANEL_TITLE="Foobar Control Panel"
 +
whereas this is a '''bad''' translation string:
 +
CONTROL PANEL=Foobar Control Panel
 +
The former works with all current versions of Joomla!, the latter doesn't.
 +
 
 +
If you have double quotation marks in your translation values, please replace them with single quotation marks. While Joomla! 1.5 allowed escaping the double quotation marks with \", Joomla! 1.6 under newer versions of PHP don't and require you to escape them as "__QQ__" which throws off Joomla! 1.5's parser. The workaround is to take advantage of a little known fact about HTML, that attributes can be wrapped in single quotation marks. For instance, change this:
 +
COM_FOOBAR_SOME_KEY=<a href="http://www.example.com">Just a test</a>
 +
to this:
 +
COM_FOOBAR_SOME_KEY="<a href='http://www.example.com'>Just a test</a>"
 +
It is valid HTML, it works on Joomla! 1.5/1.6/1.7 but it will invalidates XHTML. You can't always win, sorry.
 +
 
 +
=== Making untranslated strings look more beautiful ===
 +
 
 +
The problem with using tight translation keys as we described is that untranslated strings now look something like COM_FOOBAR_VIEWNAME_SOMEKEY. If you have volunteer translators you can bet your head that most translations will not be in sync with your component and you will have untranslated strings. In Joomla! 1.5, using "natural language" keys, you got to show your users the default (English) text if a translation key didn't exist in their language. This is impossible in Joomla! 1.6... or maybe not?
 +
 
 +
We can use the same little trick in our component's dispatcher as with our component-local translation loading string above. Here's how to load the English translation file in the backend of your component, then overwrite only the keys which exist with the user's selected language, essentially allowing untranslated keys to show in English:
 +
 
 +
<source lang="php">
 +
$jlang =& JFactory::getLanguage();
 +
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, 'en-GB', true);
 +
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, null, true);
 +
</source>
 +
 
 +
The same thing in the front-end:
 +
 
 +
<source lang="php">
 +
$jlang =& JFactory::getLanguage();
 +
$jlang->load('com_foobar', JPATH_SITE, 'en-GB', true);
 +
$jlang->load('com_foobar', JPATH_SITE, null, true);
 +
</source>
 +
 
 +
== JElement vs JFormField ==
 +
 
 +
Oh, the struggle! You need a custom widget in the configuration section of your component. In Joomla! 1.5 you could just create a new JElement. In Joomla! 1.6 you could just create a new JFormField. But both? In a single file? You can employ a simple trick. In the following example I am going to create a rather lame element, called SQL2, which displays a multi-selection box out of the results of a SQL statement. This is made as part of a fictitious module called mod_foobar
 +
 
 +
First, let your configuration know where to load the custom widget files from, by putting this in your module's XML manifest file:
 +
 
 +
<source lang="xml">
 +
<params addpath="/modules/mod_foobar/elements">
 +
<param name="ids" type="sql2" default=""
 +
label="MOD_FOOBAR_LEVELS_TITLE"
 +
description="MOD_FOOBAR_LEVELS_DESC"
 +
query="SELECT `foobar_level_id`, `title` FROM `#__foobar_levels`"
 +
key_field="foobar_level_id"
 +
value_field="title" />
 +
</params>
 +
 +
<config addfieldpath="/modules/mod_foobar/elements">
 +
<fields name="params">
 +
<fieldset name="basic">
 +
<field name="ids" type="sql2" default=""
 +
label="MOD_FOOBAR_LEVELS_TITLE"
 +
description="MOD_FOOBAR_LEVELS_DESC"
 +
query="SELECT `foobar_level_id`, `title` FROM `#__foobar_levels`"
 +
key_field="foobar_level_id"
 +
value_field="title" />
 +
</fieldset>
 +
</fields>
 +
</config>
 +
</source>
 +
 
 +
As you see, we instruct both Joomla! 1.5 and 1.6/1.7 to look into the same directory, for the same-named file (sql2.php). Here are the contents of the modules/mod_foobar/elements/sql2.php file:
 +
 
 +
<source lang="php">
 +
defined('_JEXEC') or die('Restricted Access');
 +
 
 +
/*
 +
* This trick allows us to extend the correct class, based on whether it's Joomla! 1.5 or 1.6
 +
*/
 +
if(!class_exists('JFakeElementBase')) {
 +
        if(version_compare(JVERSION,'1.6.0','ge')) {
 +
                class JFakeElementBase extends JFormField {
 +
                        // This line is required to keep Joomla! 1.6/1.7 from complaining
 +
                        public function getInput() {}
 +
                }             
 +
        } else {
 +
                class JFakeElementBase extends JElement {}
 +
        }
 +
}
 +
 
 +
/**
 +
* Our main element class, creating a multi-select list out of an SQL statement
 +
*/
 +
class JFakeElementSQL2 extends JFakeElementBase
 +
{
 +
var $_name = 'SQL2';
 +
 
 +
// Joomla! 1.5
 +
function fetchElement($name, $value, &$node, $control_name)
 +
{
 +
$db = & JFactory::getDBO();
 +
$db->setQuery($node->attributes('query'));
 +
$key = ($node->attributes('key_field') ? $node->attributes('key_field') : 'value');
 +
$val = ($node->attributes('value_field') ? $node->attributes('value_field') : $name);
 +
return JHTML::_('select.genericlist',  $db->loadObjectList(), ''.$control_name.'['.$name.'][]', 'class="inputbox" multiple="multiple" size="5"', $key, $val, $value, $control_name.$name);
 +
}
 +
 +
// Joomla! 1.6
 +
function getInput()
 +
{
 +
$db = & JFactory::getDBO();
 +
$db->setQuery($this->element['query']);
 +
$key = ($this->element['key_field'] ? $this->element['key_field'] : 'value');
 +
$val = ($this->element['value_field'] ? $this->element['value_field'] : $this->name);
 +
return JHTML::_('select.genericlist',  $db->loadObjectList(), $this->name, 'class="inputbox" multiple="multiple" size="5"', $key, $val, $this->value, $this->id);
 +
}
 +
}
 +
 
 +
/*
 +
* Part two of our trick; we define the proper element name, depending on whether it's Joomla! 1.5 or 1.6
 +
*/
 +
if(version_compare(JVERSION,'1.6.0','ge')) {
 +
        class JFormFieldSQL2 extends JFakeElementSQL2 {}
 +
} else {
 +
        class JElementSQL2 extends JFakeElementSQL2 {}               
 +
}
 +
</source>
 +
 
 +
Considering that most custom widgets have a lot more code than this, you can create two different initialisation sections for Joomla! 1.5 and 1.6/1.7 (fetchElement and getInput), populate class variables, then call a big, common method to render the bulk of the widget (just like we did with JHTML's genericlist in the example above). This will eliminate the need to rewrite the same code over again just to cater for a new Joomla! version.

Revision as of 04:07, 17 July 2011

Documentation all together tranparent small.png
Under Construction

This article or section is in the process of an expansion or major restructuring. You are welcome to assist in its construction by editing it as well. If this article or section has not been edited in several days, please remove this template.
This article was last edited by Nikosdion (talk| contribs) 12 years ago. (Purge)


It is possible to create a single installation package which can install a component in Joomla! 1.5, 1.6 and 1.7 with minor changes. Below you will find some of the most common tricks required to create an extension which is compatible with all current versions of the Joomla! CMS.

Dealing with API changes[edit]

You will regularly bump into cases where an API has changed between Joomla! 1.5, 1.6 and 1.7. In order to cater for them, you need to run slightly different code based on the Joomla! version. Right now it's possible to do that by writing conditional statements like this:

if(version_compare(JVERSION,'1.7.0','ge') {
// Joomla! 1.7 code here
} elseif(version_compare(JVERSION,'1.6.0','ge') {
// Joomla! 1.6 code here
} else {
// Joomla! 1.5 code here
}

If you do not have Joomla! 1.7-specific code -i.e it is the same as the Joomla! 1.6-specific code- please use the following code:


if(version_compare(JVERSION,'1.6.0','ge') {
// Joomla! 1.6 code here
} else {
// Joomla! 1.5 code here
}

While this conditional statement looks like an overkill, it has two major advantages:

  • Compatibility with all Joomla! versions using the same package, therefore using the same codebase, ergo less maintenance overhead
  • Once, let's say, Joomla! 1.8 is released and you decide to drop support for earlier Joomla! releases, we can just search your code for "if(version_compare(JVERSION," and remove all of the legacy code.

One XML configuration file, multiple Joomla! versions[edit]

Various XML files in Joomla! accept settings, or parameters. For example your extension manifest file, the config.xml file and the view XML files all accept such settings. You may have noticed that whereas in Joomla! 1.5 these were found under the <params> tags, in Joomla! 1.6 and later they are found inside <filedsets>. One hidden secret of Joomla! is that Joomla! 1.5 will ignore Joomla! 1.6 syntax, whereas Joomla! 1.6/1.7 will ignore Joomla! 1.5 syntax. This allows you to have a single XML file which caters for all Joomla! releases. For example, here's a sample config.xml file, to be placed in the component's backend directory:

<?xml version="1.0" encoding="utf-8"?>
<config>
	<params>
		<!-- Joomla! 1.5 uses params -->
		<param name="foobar" type="text" default="" size="30"
			label="COM_FOOBAR_OPTION_FOOBAR_LBL"
			description ="COM_FOOBAR_OPTION_FOOBAR_DESC" />
	</params>

	<fieldset>
		<!-- Joomla! 1.6 uses fieldset -->
		name="basic"
		label="COM_FOOBAR_OPTIONS_SECTION_BASIC_LBL"
		description="COM_FOOBAR_OPTIONS_SECTION_BASIC_DESC"
		>
		<field name="foobar" type="text" default="" size="30"
			label="COM_FOOBAR_OPTION_FOOBAR_LBL"
			description ="COM_FOOBAR_OPTION_FOOBAR_DESC" />
	</fieldset>
</config>

That's how easy it is!

Dealing with languages[edit]

How to load component-local translation files in Joomla! 1.5[edit]

Joomla! 1.6 introduced a new feature where a user can place language files inside the component's directory, e.g. administrator/com_foobar/language/en-GB/en-GB.com_foobar.ini, in order to provide language overrides. But Joomla! 1.5 doesn't support this feature. Or does it? Even though it can not do that automatically, you can do so in your extension's dispatcher file, e.g. administrator/com_foobar/foobar.php. Here's the magic code to do that in the backend:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, null, true);
$jlang->load('com_foobar', JPATH_COMPONENT_ADMINISTRATOR, null, true);

and the frontend:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_SITE, null, true);
$jlang->load('com_foobar', JPATH_COMPONENT, null, true);

Tips and tricks for language strings[edit]

All your translation keys should consist of uppercase characters without spaces (that is A-Z, 0-9 and underscores). All translation values should be included in double quotation marks. This allows the translation files to be parsed by Joomla! 1.5/1.6/1.7 alike. You are also suggested to prefix your translation keys with the extension's name, albeit this is not required. For example, this is a good translation string: COM_FOOBAR_CPANEL_TITLE="Foobar Control Panel" whereas this is a bad translation string: CONTROL PANEL=Foobar Control Panel The former works with all current versions of Joomla!, the latter doesn't.

If you have double quotation marks in your translation values, please replace them with single quotation marks. While Joomla! 1.5 allowed escaping the double quotation marks with \", Joomla! 1.6 under newer versions of PHP don't and require you to escape them as "__QQ__" which throws off Joomla! 1.5's parser. The workaround is to take advantage of a little known fact about HTML, that attributes can be wrapped in single quotation marks. For instance, change this: COM_FOOBAR_SOME_KEY=<a href="http://www.example.com">Just a test</a> to this: COM_FOOBAR_SOME_KEY="<a href='http://www.example.com'>Just a test</a>" It is valid HTML, it works on Joomla! 1.5/1.6/1.7 but it will invalidates XHTML. You can't always win, sorry.

Making untranslated strings look more beautiful[edit]

The problem with using tight translation keys as we described is that untranslated strings now look something like COM_FOOBAR_VIEWNAME_SOMEKEY. If you have volunteer translators you can bet your head that most translations will not be in sync with your component and you will have untranslated strings. In Joomla! 1.5, using "natural language" keys, you got to show your users the default (English) text if a translation key didn't exist in their language. This is impossible in Joomla! 1.6... or maybe not?

We can use the same little trick in our component's dispatcher as with our component-local translation loading string above. Here's how to load the English translation file in the backend of your component, then overwrite only the keys which exist with the user's selected language, essentially allowing untranslated keys to show in English:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, 'en-GB', true);
$jlang->load('com_foobar', JPATH_ADMINISTRATOR, null, true);

The same thing in the front-end:

$jlang =& JFactory::getLanguage();
$jlang->load('com_foobar', JPATH_SITE, 'en-GB', true);
$jlang->load('com_foobar', JPATH_SITE, null, true);

JElement vs JFormField[edit]

Oh, the struggle! You need a custom widget in the configuration section of your component. In Joomla! 1.5 you could just create a new JElement. In Joomla! 1.6 you could just create a new JFormField. But both? In a single file? You can employ a simple trick. In the following example I am going to create a rather lame element, called SQL2, which displays a multi-selection box out of the results of a SQL statement. This is made as part of a fictitious module called mod_foobar

First, let your configuration know where to load the custom widget files from, by putting this in your module's XML manifest file:

	<params addpath="/modules/mod_foobar/elements">
		<param name="ids" type="sql2" default=""
			label="MOD_FOOBAR_LEVELS_TITLE"
			description="MOD_FOOBAR_LEVELS_DESC"
			query="SELECT `foobar_level_id`, `title` FROM `#__foobar_levels`"
			key_field="foobar_level_id"
			value_field="title" />
	</params>
	
	<config addfieldpath="/modules/mod_foobar/elements">
		<fields name="params">
			<fieldset name="basic">
				<field name="ids" type="sql2" default=""
					label="MOD_FOOBAR_LEVELS_TITLE"
					description="MOD_FOOBAR_LEVELS_DESC"
					query="SELECT `foobar_level_id`, `title` FROM `#__foobar_levels`"
					key_field="foobar_level_id"
					value_field="title" />
			</fieldset>
		</fields>
	</config>

As you see, we instruct both Joomla! 1.5 and 1.6/1.7 to look into the same directory, for the same-named file (sql2.php). Here are the contents of the modules/mod_foobar/elements/sql2.php file:

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

/*
 * This trick allows us to extend the correct class, based on whether it's Joomla! 1.5 or 1.6
 */
if(!class_exists('JFakeElementBase')) {
        if(version_compare(JVERSION,'1.6.0','ge')) {
                class JFakeElementBase extends JFormField {
                        // This line is required to keep Joomla! 1.6/1.7 from complaining
                        public function getInput() {}
                }               
        } else {
                class JFakeElementBase extends JElement {}
        }
}

/**
 * Our main element class, creating a multi-select list out of an SQL statement
 */
class JFakeElementSQL2 extends JFakeElementBase
{
	var	$_name = 'SQL2';

	// Joomla! 1.5
	function fetchElement($name, $value, &$node, $control_name)
	{
		$db			= & JFactory::getDBO();
		$db->setQuery($node->attributes('query'));
		$key = ($node->attributes('key_field') ? $node->attributes('key_field') : 'value');
		$val = ($node->attributes('value_field') ? $node->attributes('value_field') : $name);
		return JHTML::_('select.genericlist',  $db->loadObjectList(), ''.$control_name.'['.$name.'][]', 'class="inputbox" multiple="multiple" size="5"', $key, $val, $value, $control_name.$name);
	}
	
	// Joomla! 1.6
	function getInput()
	{
		$db			= & JFactory::getDBO();
		$db->setQuery($this->element['query']);
		$key = ($this->element['key_field'] ? $this->element['key_field'] : 'value');
		$val = ($this->element['value_field'] ? $this->element['value_field'] : $this->name);
		return JHTML::_('select.genericlist',  $db->loadObjectList(), $this->name, 'class="inputbox" multiple="multiple" size="5"', $key, $val, $this->value, $this->id);
	}
}

/*
 * Part two of our trick; we define the proper element name, depending on whether it's Joomla! 1.5 or 1.6
 */
if(version_compare(JVERSION,'1.6.0','ge')) {
        class JFormFieldSQL2 extends JFakeElementSQL2 {}
} else {
        class JElementSQL2 extends JFakeElementSQL2 {}                
}

Considering that most custom widgets have a lot more code than this, you can create two different initialisation sections for Joomla! 1.5 and 1.6/1.7 (fetchElement and getInput), populate class variables, then call a big, common method to render the bulk of the widget (just like we did with JHTML's genericlist in the example above). This will eliminate the need to rewrite the same code over again just to cater for a new Joomla! version.