Programatical Drupal 7 setup of taxonomy with i18n
This article describes how to define and set up a vocabulary and its terms programatically in the international (i18n) or multilingual way.
Motivation
Say, if an administrative module requires a taxonomy-type data structure, it sometimes makes sense to use a built-in taxonomy. It is well possible to make a module-specific database table for it, of course. A major advantage to use a built-in taxonomy is that each user (most likely an administrator of the site, as s/he needs a permission to access the taxonomy) can change the words etc via the built-in user interface if they want. To provide the functionality of this kind is both troublesome for module developers, and those functionality, if provided, may be counter-intuitive for users.
File for the code
To achieve it, module developers must set up the required vocabulary and its terms programatically. The best place to write the code is in a function hook_install(), that is MY_MODULE_NAME_install(), placed in the file
MY_MODULE_NAME/MY_MODULE_NAME.install.inc
Also, it is a good idea to define the function hook_uninstall() too in the file to delete the module-specific vocabulary.
Strategy
How do you want to use the vocabulary in your module? i18n can be defined in many levels, and so the strategy varies a lot. The i18n module offers several options, including
- Non-translatable (i18n_mode of I18N_MODE_NONE, as defined in /sites/all/modules/i18n/i18n.module),
- Localized, i.e., each term must be translated (I18N_MODE_LOCALIZE),
- Translatable but not localizable, i.e., each language may have a different set of terms per vocabulary, and some terms can be related to a translation to each other (I18N_MODE_TRANSLATE)
For example, both the following two senarios are possible:
- Vocabulary A has 6 terms, 3 of which are for English and the rest (3) are for Japanese. They may be a translation or not.
- Vocabulary A are for English only and Vocabulary B are for Japanese only. Each of them has 3 terms, which may be a translation or not.
In addition, vocabulary can allow each of its term to have multiple fields. Each of those fields may be translatable or not. If translatable, how?
To cover all of them would be a way beyond a scope of this article. Instead, the following is the i18n setting I describe here.
- Considering only a single vocabulary (Vocabulary A) and its terms, that is a single vid and machine_name. The translation of the vocabulary itself is NOT considered.
- Vocabulary A contains a certain fixed number of terms, regardless user's language setting. In other words, a translation of a term is not assigned with a different tid. If Vocabulary contains 6 terms, it contains 6 tids.
- Each term is translatable, though some are allowed to be in Language-Neutral. This practically means its name and description can be translatable.
- Any fields in the term is translatable, though some fields are allowed to be in Language-Neutral.
Then, if you see a list of the terms in Vocabulary A, you see the same number of terms, regardless of the language environment. If your language environment is English, the term name and fields are in English, and if Japanese, they are Japanese.
To achieve this, especially the flexible translation of each field, the entity_translation module is required (enabled), and i18n_mode must be set to be I18N_MODE_ENTITY_TRANSLATION, which is defined in the module (php code: /sites/all/modules/entity_translation/entity_translation.module). Also, make sure in the configuration of the module (/admin/config/regional/entity_translation), Taxonomy term
in Section Translatable entity types
is activated.
A downside of the entity_translation is that it renders the name and description of each term, as well as the title of nodes, untranslatable. To circumvent it, another module title must be installed and enabled (IF you want those translatable). Once enabled, activate Name
(and Description
) in Automatic field replacement
for the Taxonomy Term
tab in its configuration (/admin/config/content/title).
Also, the following 4 modules should be enabled:
- field_ui
- i18n
- i18n_field
- i18n_taxonomy
Our example
We here assume the source language is English, and the second language is Japanese. The terms in the vocabulary, defined first in English, are translated into Japanese.
The following example is in the module my_fav_food_vocab. It registers the vocabulary My Fav Fruits
in Taxonomy with the machine name of my_fav_food_vocab. The vocabulary contains 2 terms with the names of Apple
and Lemon
in English, and both names are translated into Japanese, so that a user in the language environment of Japanese would see the Japanese name. Each term contains 2 fields of Color
and Taste
. For example, the Color
field of the term Apple
has the value of Red
in English, which is translated into Japanese 赤
.
The title (and description) of the vocabulary and those of the 2 fields are not translated (though they can be, techinically).
The following is the diagram, where en
, ja
, ALL
indicates English, Japanese, All-languages:
Vocabulary 1: * Machine-name: 'my_fav_food_vocab' * Name: 'My Fav Fruits'[ALL] * Terms: * Term 1: * Name: 'Apple'[en] / '林檎'[ja] * Fields: * Field 1: * Name: 'Color'[ALL] * Value: 'Red'[en] / '赤'[ja] * Field 2: * Name: 'Taste'[ALL] … * Term 2: * Name: 'Lemon'[en] / '檸檬'[ja] …
Code
Now I present a set of the code snippets and descriptions.
Preparation
Let us set up a global constant for the vocabulary name for convenience:
if (!defined('MY_FAV_FOOD_VOCAB')) {
define( 'MY_FAV_FOOD_VOCAB', 'my_fav_food_vocab');
}
Creates a taxonomy vocabulary
First, let's creat the vocabulary. A comment in Drupal website is useful for vocabulary creation, and another for i18n.
$new_vocab = taxonomy_vocabulary_machine_name_load(MY_FAV_FOOD_VOCAB);
if (! $new_vocab) {
$new_vocab = new stdClass();
$new_vocab->name = 'My Fav Fruits';
$new_vocab->machine_name = MY_FAV_FOOD_VOCAB;
$new_vocab->description = 'My own description';
$new_vocab->module = 'taxonomy';
$new_vocab->i18n_mode = I18N_MODE_ENTITY_TRANSLATION;
taxonomy_vocabulary_save($new_vocab);
}
The last line in setting the object is the key to setup the i18n environment.
Note the variable $new_vocab would not be NULL if it already exists. You may choose to halt here (with a warning) if the vocabulary already exists.
Builds custom fields for the terms in the vocabulary
The overall procedure is as follows.
- Creates a field (which can be used for anything, techinically).
- Creates a field instance so that the created field is associated to the term.
Let's look at them step by step and then integrate them.
Creating a field
Here is an example to make a simple text field.
function _my_fav_food_make_field($field_name) {
$field_prm = array(
'field_name' => $field_name,
'type' => 'text',
'label' => t('Label'), // for what?
'translatable' => TRUE,
// 'settings' => array('max_length' => 80),
);
field_create_field($field_prm);
}
Here, the translatable parameter is the key to make it i18n.
Associating the field to the term
function _my_fav_food_associate_field($field_name, $label) {
$instance = array(
'field_name' => $field_name,
'entity_type' => 'taxonomy_term',
'bundle' => MY_FAV_FOOD_VOCAB,
'label' => $label,
'description' => 'Text for ' . $label,
'required' => TRUE,
'widget' => array(),
'i18n_mode' => I18N_MODE_ENTITY_TRANSLATION,
);
$arret['widget'] = array_merge($arret['widget'], $arall[$field_text]['widget']);
if ($bundle) {
$arret['bundle'] = $bundle;
}
field_create_instance($instance);
}
Here, the field_name and bundle parameters describe which field is made associated to the terms (as specified in entity_type) in which vocabulary, respectively.
Integrating them
Here is a wee example code to integrate them.
$ar_fields = array(
'Color' => 'field_my_fav_food_color',
'Taste' => 'field_my_fav_food_taste',
);
foreach ($ar_fields as $label => $field_name) {
if (field_info_field($field_name)) {
$msg = sprintf('Field (%s) already exists! Skips.', $field_name);
drupal_set_message($msg, 'warning');
continue;
}
// Creates a custom field.
_my_fav_food_make_field($field_name);
// Associates the field to the terms in the vocabulary.
_my_fav_food_associate_field($field_name, $label);
}
Creating a term and setting its fields
Creating a term
First, you create a term. Here is an example to creat a new term programatically (see a comment in Drupal site).
function _my_fav_food_create_a_term($vid, $term_name) {
$interm = new stdClass();
$interm->name = $term_name; // The name of the term
$interm->description = ucfirst($term_name); // Description
$interm->vid = $vid; // The ID of the parent vocabulary
$interm->language = 'en'; // Source language.
$interm->parent = 0; // This is a top-level term
taxonomy_term_save($interm);
return $interm->tid;
}
This function returns the term-ID (tid).
Only the thing this function does is to create a term with its term name and description in the source language. No field parameters for the term are set.
Add a translation for the term (name and description)
function _my_fav_food_add_term_translation($term, $name, $trans_to) {
$handler = entity_translation_get_handler('taxonomy_term', $term);
// gets the translation handler in place
$translation = array(
'translate' => 0,
'status' => 1,
'source' => 'en', // the source language
'language' => 'ja', // the language you're translating to
);
$values = array(
$name => array(
'ja' => array(
array( 'value' => $trans_to, )
)
)
);
$handler->setTranslation($translation, $values);
}
Here, the argument $name should be, for the name of the term, either
- name_field if the title module is enabled and set up accordingly, or
- name if not.
The name of the description changes, accordingly, too.
If $name = 'name' is given to the function (which should be so when title module is not activated), this function silently fails to register the translation for the name. In that case, therefore, any dummy string for $trans_to would be fine. Note that it is still necessary to run it in order to register the filed translation, which is described below.
Setting parameters, including translations, of a field
Let us assume the associated array $values has a key of the official langcode (language code) string, e.g., en (English) and ja (Japanese), for the value. For example,
$values = array(
'en' => 'red',
'ja' => '赤',
);
Then,
function _my_fav_food_set_field($term, $field_name, $values) {
foreach (array_keys($values) as $ea_lang) {
// Each langcode
$newterm->{$field_name}[$la][0]['value'] = $value[$ea_lang];
}
taxonomy_term_save($term);
}
Integrating the creation of terms and their associated fields
Finally, here is the code to integrate those three parts, creating a term and setting its associated fields in the i18n way.
$trans_tos = array(
'Apple' => '林檎',
'Lemon' => '檸檬',
);
$prms = array(
'Apple' => array(
'Color' => array(
'en' => 'red',
'ja' => '赤',
),
'Taste' => array(
'en' => 'Sweet',
'ja' => '甘い',
),
),
'Lemon' => array(
'Color' => array( 'en' => 'yellow', 'ja' => '黄', ),
'Taste' => array( 'en' => 'Sour', 'ja' => '酸っぱい', ),
),
);
// Gets Vocabulary ID
$vid = $new_vocab->vid;
// Creating two terms: 'Apple' and 'Lemon'
foreach ($trans_tos as $tname => $trans_to) {
// Creates a term
$tid = _my_fav_food_create_a_term($vid, $tname);
// Loads the created term
$term = taxonomy_term_load($tid);
foreach ($ar_fields as $label => $field_name) {
// Sets field parameters (in multiple languages)
$values = $prms[$tname][$label];
_my_fav_food_set_field($term, $field_name, $values);
}
// Sets the translation (of the name) for the term.
// This registers the field translation as well.
// Note this assumes the "title" module is activated.
// If not, replace "name_field" with "name".
_my_fav_food_add_term_translation($term, 'name_field', $trans_to);
// Finally, saves everything in the database to finalize it.
taxonomy_term_save($term);
}
Summary
In summary, here is the entier code my_fav_food/my_fav_food.install.inc for the module my_fav_food. It integrating all the above to programmatically create a vocabulary and its terms with fields in the 18n way, as well as its deletion at the timing of uninstall.
<?php
//
// Vocabulary, terms and fields data to set.
//
if (!defined('MY_FAV_FOOD_VOCAB')) {
define( 'MY_FAV_FOOD_VOCAB', 'my_fav_food_vocab');
}
/**
* Implements hook_uninstall().
*/
function my_fav_food_uninstall() {
// Uninstall a vocabulary 'my_fav_food_vocab'
$vocab = taxonomy_vocabulary_machine_name_load(MY_FAV_FOOD_VOCAB);
if ($vocab) {
taxonomy_vocabulary_delete($vocab->vid);
}
}
/**
* Implements hook_install().
*/
function my_fav_food_install() {
// Term names (with its translations)
$trans_tos = array(
'Apple' => '林檎',
'Lemon' => '檸檬',
);
// Field machine names
$ar_fields = array(
'Color' => 'field_my_fav_food_color',
'Taste' => 'field_my_fav_food_taste',
);
// Field values (with its translations)
$prms = array(
'Apple' => array(
'Color' => array(
'en' => 'red',
'ja' => '赤',
),
'Taste' => array(
'en' => 'Sweet',
'ja' => '甘い',
),
),
'Lemon' => array(
'Color' => array( 'en' => 'yellow', 'ja' => '黄', ),
'Taste' => array( 'en' => 'Sour', 'ja' => '酸っぱい', ),
),
);
//
// Creates a taxonomy vocabulary
//
$new_vocab = taxonomy_vocabulary_machine_name_load(MY_FAV_FOOD_VOCAB);
if (! $new_vocab) {
$new_vocab = new stdClass();
$new_vocab->name = 'My Fav Fruits';
$new_vocab->machine_name = MY_FAV_FOOD_VOCAB;
$new_vocab->description = 'My own description';
$new_vocab->module = 'taxonomy';
$new_vocab->i18n_mode = I18N_MODE_ENTITY_TRANSLATION;
taxonomy_vocabulary_save($new_vocab);
}
else {
// Does something (maybe return NULL)
}
//
// Builds custom fields for the terms in the vocabulary
//
foreach ($ar_fields as $label => $field_name) {
if (field_info_field($field_name)) {
$msg = sprintf('Field (%s) already exists! Skips.', $field_name);
drupal_set_message($msg, 'warning');
continue;
}
// Creates a custom field.
_my_fav_food_make_field($field_name);
// Associates the field to the terms in the vocabulary.
_my_fav_food_associate_field($field_name, $label);
}
//
// Creating/Setting terms and their associated fields
//
// Gets Vocabulary ID
$vid = $new_vocab->vid;
// Creating two terms: 'Apple' and 'Lemon'
foreach ($trans_tos as $tname => $trans_to) {
// Creates a term
$tid = _my_fav_food_create_a_term($vid, $tname);
// Loads the created term
$term = taxonomy_term_load($tid);
foreach ($ar_fields as $label => $field_name) {
// Sets field parameters (in multiple languages)
$values = $prms[$tname][$label];
_my_fav_food_set_field($term, $field_name, $values);
}
// Sets the translation (of the name) for the term.
// This registers the field translation as well.
// Note this assumes the "title" module is activated.
// If not, replace "name_field" with "name".
_my_fav_food_add_term_translation($term, 'name_field', $trans_to);
// Finally, saves everything in the database to finalize it.
taxonomy_term_save($term);
}
} // End of function my_fav_food_install()
/**
* Implements private functions.
*/
//
// Creates a field
//
function _my_fav_food_make_field($field_name) {
$field_prm = array(
'field_name' => $field_name,
'type' => 'text',
'label' => t('Label'), // for what?
'translatable' => TRUE,
// 'settings' => array('max_length' => 80),
);
field_create_field($field_prm);
}
//
// Associates a field
//
function _my_fav_food_associate_field($term, $field_name, $values) {
foreach (array_keys($values) as $ea_lang) {
// Each langcode
$newterm->{$field_name}[$la][0]['value'] = $value[$ea_lang];
}
taxonomy_term_save($term);
}
//
// Creates/Sets a term
//
function _my_fav_food_create_a_term($vid, $term_name) {
$interm = new stdClass();
$interm->name = $term_name; // The name of the term
$interm->description = ucfirst($term_name); // Description
$interm->vid = $vid; // The ID of the parent vocabulary
$interm->language = 'en'; // Source language.
$interm->parent = 0; // This is a top-level term
taxonomy_term_save($interm);
return $interm->tid;
}
//
// Sets a field
//
function _my_fav_food_set_field($term, $field_name, $values) {
foreach (array_keys($values) as $ea_lang) {
// Each langcode
$newterm->{$field_name}[$la][0]['value'] = $value[$ea_lang];
}
taxonomy_term_save($term);
}
Acknowledgment
Rendering of this page utilizes the prettify package developed by Google, providing your browser decides to load it.