4
\$\begingroup\$

I am building a php based mobile website that must be in both Spanish and English. This is my first bilingual site so I've been having to re-think a lot of coding practices I currently use to accommodate multiple languages.

I'm using a custom MVC pattern and currently I'm trying to use the same controller and model for both Spanish and English version of the site and just change the view for each language.

Since I'm using same model for both Spanish and English I have created an error system in the model that supports both languages see sample below.

The error list will be different depending on the model but I have 1 model with almost 30 errors and 60 translations. Its a big Form with a lot of input field validations.

Do you think it would be better to keep it as 1 large nested array or split the array into something like $english_errorids and $spanish_errorids

Open to any ideas you guys may have, it currently works as is I'm just trying to make sure I'm doing this the right way.

 /**
 * Created by PhpStorm.
 * User: wmcgee
 * Date: 11/19/14
 * Time: 12:05 PM
 */

 class SampleModel
 {
     #region Error System
     /** 
      * List of all recorded errors
      * @var array 
      */
     public $errors;

     /**
      * Can be set to either ENG or SPAN
      * @var string
      */
     public $lang = "ENG";

     /**
      * List of errors with translations 
      * @var array 
      */
     public $errorids = array(
         10 => array('ENG'=>"Vehicle Condition Missing", 'SPAN'=>"Condicion de vehiculo falta"),
         11 => array('ENG'=>"Signature Missing",         'SPAN'=>"Falta su firma "),
     );

     /**
      * Records a translated error based on what lang is set and
      * what error id was passed into recorded errors list.
      * 
      * @param $id int
      * @param bool $override if true id is treated as an 
      *                       error message and is recorded
      *                       in error list.
      */
     public function recordError($id, $override = false)
     {
         //- Allow manual error messages not in translation list to be added
         if( $override ){
             $this->errors[] = $id;
             return;
         }

         $this->errors[] = $this->errorids[$id][$this->lang];
     }

    /**
     * Returns errors formatted in HTML
     * @return HTML
     */
    public function getErrors()
    {
        if( sizeof($this->errors) == 0){ return ''; }

        $msg = '<p class="errorBox"><b>Report Errors</b><br>';

        foreach($this->errors as $error)
        {
            $msg .= "&bull; " . $error . "<br>";
        }

        $msg .= "</p>";

        return $msg;
    }
     #endregion
 }
\$\endgroup\$
  • \$\begingroup\$ Interesting application, don't see multi-lingual designs very often! +1 \$\endgroup\$ – Phrancis Nov 19 '14 at 19:23
  • \$\begingroup\$ I'd suggest putting all errors-ids as indexed array in separate files en.php and sp.php and just require_once as per your need. \$\endgroup\$ – hjpotter92 Nov 19 '14 at 21:16
  • \$\begingroup\$ The logic is the same, so having multiple files is a bad way to go. Given my country of origin, every project we work on needs to support at least 3 languages. The best way to do this is to translate the output where it is being created: the view. Just have a generic error message (with sprintf placeholders if needed) and use a translator service. In your, just write $this->translate($data->something); instead of <?= $data->something; ?> and you're done \$\endgroup\$ – Elias Van Ootegem Nov 21 '14 at 11:05
3
\$\begingroup\$

I have had this problem several times and have tried different solutions. This is my current solution.

I use an associative array as a key-value list of message strings. Then I set the errors to a specific key name and fetch the corresponding message in a appropriately named file like english.php etc. This file can be all types of formats, but I will stick with PHP.

This has two main advantages:

  • Human readable key names
  • A central place all message string are stored, which in turn makes reuse of messages easier.

An example would be:

/*
* File: english.php
* This must return an associative array. This array can be multidimentional to better
* group different messages.
*/
$lang = [];
$lang['field_required'] = 'The field is required.';
$lang['success'] = 'The post was successfully created.';

// A grouped entry.
$lang['registration']['email_unavailable'] = 'The provided email is already in use.';

/*
 * A message string that requires some input that cannot be determined beforehand can 
 * be solved by using the sprintf() or vsprintf() functions with the latter using an
 * array as an argument.
 */
$lang['shopping']['cart']['items']['amount_of_items'] = 'There are %d item(s) in your shopping cart.';

return $lang;

/*
 * File: bootstrapper.php (example)
 * 
 * Some centrally available file. The $languages variable should be provided to
 * classes using dependency injection or made available otherwise.
 */
$current_language = 'english'; // Detected in a class or some other code.

$file = "path/to/language/{$current_language}.php"; // This file must return an array.

/*
 * Include the currently supported language file.
 * You can also make a list of supported languages before trying to load the file.
 */
if(!is_file($file) || !is_readable($file)) {
    // Do something here...
}

$languages = include $file;

/* 
 * (OPTIONAL)
 * Here you can check if the language file returned the expected format. 
 * Example: if(!is_array($languages)) { // do something }
 */

/*
 * Some model or controller (if you're validating input before sending it to the
 * model).
 * Here the language key is the same for all languages and only the value is 
 * different.
 */
 $field_id = 'username';
 $field = filter_input(INPUT_POST, $field_id, FILTER_SANITIZE_STRING); // Just as good practice

 if(!check_required_field_function($field)) {
     $errors[$field_id] = $languages['field_required'];
 }

 /*
  * File: some view.php
  * Since this file properly contains some HTML I have used an alternative PHP syntax
  */
 <?php if(isset($errors) && !empty($errors)): ?>

     <p>The following errors occurred.</p>
     <ul>         

     <?php foreach($errors as $error): ?>

         <li><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></li> 

     <? endforeach; ?>

     </ul>

 <?php endif; ?>

The output escaping isn't stictly related to this question, but I added it as good practice.

This approach can properly be refactored a lot. Please let me know if you have any questions or something you would point out.

This is a very simple outline of the strategy I use. Hope it helps.

\$\endgroup\$
2
\$\begingroup\$

One widely accepted way to handle translations is using GNU gettext utilities. It requires that PHP be configured for gettext, so check that your environment supports it.

http://php.net/manual/en/book.gettext.php

\$\endgroup\$
1
\$\begingroup\$
 public $lang = "ENG";

The more common name is $locale, which has values like en_US or es_MX. This allows you to offer different messages for British and American users. Because this is standard, there will often be easy ways to set it.

 public $errorids = array(
     10 => array('ENG'=>"Vehicle Condition Missing", 'SPAN'=>"Condicion de vehiculo falta"),
     11 => array('ENG'=>"Signature Missing",         'SPAN'=>"Falta su firma "),
 );

Having a numeric component can help with translation. For example, if you have both fields meaning entries in an HTML form and fields meaning land under cultivation on a farm, then you don't want them to have the same field_required translation key. That said, people often want the translation keys to give some hint as to what the text might be, so they mix the numeric ID with a string, e.g. vehicle_condition_missing_10.

You probably want to separate your main code from the translations. Also, you probably want to separate your English and Spanish translations from each other. The reason is that you don't want to have to change the file with the English translations or the code because there is a problem with the Spanish translation. This also allows you to use separate encodings for each translation. And of course, separate files scales well.

I'd be wary of trying to use generic English messages to cover multiple situations. For example, there may be a language that uses different words to describe a form entry that can hold a string like a name versus a number. It's often better to be as specific as possible in your message so as to save yourself the trouble of refactoring when a new language causes translation problems. Repeating yourself makes for bad coding practice, but it may be good for translating.

Be especially leery of embedding translated strings in other translated strings. It's very easy to get into situations where (for example) two strings use different articles or verbs. An English example would be "I am", "you are", or "she is". "I is" or "you am" would sound ridiculous but is the kind of thing that string embedding gets you. Also be careful of concatenating strings. The translated version may put the words in a different order. For example, Spanish often puts the adjective after the noun.

\$\endgroup\$
  • \$\begingroup\$ Thanks, I changed the locale keys and separated the arrays into their own respected languages. thanks everyone for your input \$\endgroup\$ – McGee.WIll Nov 20 '14 at 19:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.