views:

3684

answers:

9

Hello,

I'm developing a PHP application and I'm wondering about the best way to include multi-language support for users in other countries.

I'm proficient with PHP but have never developed anything with support for other languages.

I was thinking of putting the language into a PHP file with constants, example:

en.php could contain:

define('HZ_DB_CONN_ERR', 'There was an error connecting to the database.');

and fr.php could contain:

define('HZ_DB_CONN_ERR', 'whatever the french is for the above...');

I could then call a function and automatically have the correct language passed in.

hz_die('HZ_DB_CONN_ERR', $this);

Is this a good way of going about it?

-- morristhebear.

+2  A: 

You might want to look at a framework like CakePHP or CodeIgniter that make writing internationalized applications much easier. It's not just strings you have to consider -- things like number formats and date formats also have to be accounted for

Conrad
I'm generally not a fan of full MVC frameworks (I do like to use Smarty though), I'm still in the early stages of designing this and hadn't yet thought about currency, date formats but will take this on board... thanks!
morristhebear
IMHO CakePHP is complete overkill (and bloat like 99% of MVC frameworks).
cletus
+5  A: 

You can use gettext or something which supports gettext as well as more such as Zend_Translate.

Edit:

Just for precision, Zend_Translate supports gettext without the gettext module. You can see here the many different types of input it supports.

If you use arrays as also suggested you can also use this with Zend_Translate. The point being, if you use arrays today and gettext, xml or something else tomorrow you only have to change your config for Zend_Translate.

OIS
A: 

Your approach is workable, in the case where you include different constantfiles depending on which language the user has choosen.

You could also skip the constant-part and just define a big hashtable with constant => translation to prevent crashes in namespace.

file: en.php

$constants = Array(
 'CONSTANT_KEY' => 'Translated string'
);

file: functions.php

function getConstant($key, $fallback) {
   // ... return constant or fallback here.
}

However, when it comes to large amount of data, this approach will be hard to maintain. There are a few other approaches that might serve your purpose better, for instance, an ideal solution is where all your constants are stored in a global memoryspace for your entire site, to avoid having each and every reguest/thread keeping all this translated data in memory. This requires some sort php module-approach. Gettext as someone here suggested might use that approach.

Google for PHP localization to find useful resources.

jishi
+1  A: 

Your solution should work fine as long as it is solely intended for translating. As others have mentioned, there are many other locale-based variables, such as currency and date formats.

A solid approach to making your application locale-proof would be to use Zend_Locale combined with Zend_Translate.

Zend_Locale allows you to easily detect the user's locale, or set it if you wish. This class is useful for automatically setting the correct currency format, for example.

Zend_Translate allows you to easily translate text using several different formats:

  • Array
  • CSV
  • Gettext
  • Ini
  • Tbx
  • Tmx
  • Qt
  • Xliff
  • XmlTm
Aron Rotteveel
A: 

One tip for you, is to create a function that returns the translated string, and if it is not in the hashtable, then return the hash key you requested with something like a * behind it to notify you it needs translation.

Ólafur Waage
+4  A: 

Weird. People seem to be ignoring the obvious solution. Your idea of locale-specific files is fine. Have en.php:

define('LOGIN_INCORRECT','Your login details are incorrect.');
...

Then, assuming you have a global config/constants file (which I'd suggest for many reasons), have code like this:

if ($language == 'en') {
  reqire_once 'en.php';
} else if ($language == 'de') {
  require_once 'de.php';
}

You can define functions for number and currency display, comparison/sorting (ie German, French and English all have different collation methods), etc.

People often forget PHP is a dynamic language so things like this:

if ($language == 'en') {
  function cmp($a, $b) { ... }
} else if ($language == 'de') {
  function cmp($a, $b) { ... }
}

are in fact perfectly legal. Use them.

cletus
Nice thought, I agree with this approach
Codex73
A: 

Here is how I do it:

parent_script.php:

$lang_pick = "EN";    //use your own method to set language choice

require_once('trans_index.php'); 

echo $txt['hello'];


trans_index.php :

    $text = array();

    $text['hello'] = array (
     "EN"=> "Hello",
     "FR"=> "Bonjour",
     "DE"=> "Guten Tag",
     "IT"=> "Ciao"
    );                          //as many as needed



foreach($text as $key => $val) {

$txt[$key] = $text[$key][$lang_pick];

}

That may be too simple for your needs, but I find it very workable. It also makes it very easy to maintain the multiple versions of the text.

A: 

I really love the following approach:

one file is the translator itself:

class Translator{
    private static $strs = array();
    private static $currlang = 'en';

    public static function loadTranslation($lang, $strs){
        if (empty(self::$strs[$lang]))
            self::$strs[$lang] = array();

        self::$strs[$lang] = array_merge(self::$strs[$lang], $strs);        
    }

    public static function setDefaultLang($lang){
        self::$currlang = $lang;        
    }

    public static function translate($key, $lang=""){
        if ($lang == "") $lang = self::$currlang;
        $str = self::$strs[$lang][$key];
        if (empty($str)){
            $str = "$lang.$key";            
        } 
        return $str;       
    }    

    public static function freeUnused(){
        foreach(self::$strs as $lang => $data){
            if ($lang != self::$currlang){
                $lstr = self::$strs[$lang]['langname'];
                self::$strs[$lang] = array();
                self::$strs[$lang]['langname'] = $lstr;                
            }            
        }        
    }

    public static function getLangList(){
        $list = array();
        foreach(self::$strs as $lang => $data){
            $h['name'] = $lang;
            $h['desc'] = self::$strs[$lang]['langname'];
            $h['current'] = $lang == self::$currlang;
            $list[] = $h;
        }
        return $list;        
    }

    public static function &getAllStrings($lang){
        return self::$strs[$lang];
    }

}

function generateTemplateStrings($arr){
    $trans = array();
    foreach($arr as $totrans){
        $trans[$totrans] = Translator::translate($totrans);
    }
    return $trans;    
}

the language-files can be simply include()d and look like this:

en.php:

Translator::loadTranslation('en', array(
  'textfield_1'          => 'This is some Textfield',
  'another_textfield '   => 'This is a longer Text showing how this is used',
));

de.php:

Translator::loadTranslation('de', array(
  'textfield_1'          => 'Dies ist ein Textfeld',
  'another_textfield '   => 'Dies ist ein längerer Text, welcher aufzeigt, wie das hier funktioniert.',
));

in you app you can do either translate one string like this:

$string = Translator::translate('textfield_1')

or even a bunch of strings:

$strings = generateTemplateStrings(array('textfield_1', 'another_textfield'));

Because the language-files can just be included, you can stack the very easily and, say, include a global file first and then include files from submodules which can either add new strings or replace already defined ones (which doesn't work with the define()-method).

Because it's pure PHP, it can even be opcode-cached very easily.

I even have scripts around which generate CSV-files containing untranslated strings from a file and even better: convert the translated CSV-file back to the language file.

I have this solution in productive use since 2004 and I'm so very happy with this.

Of course, you can even extend it with conventions for pluralizing for example. Localizing number formats is something you'd have to do using other means though - the intl-extension comes to mind.

pilif
+2  A: 

Thanks to all the people for your approaches and solutions. I came here with the same beggining doubt, and i can conlude, that to use the "define method" is more realistic, as, even with the odd problem that doesn't use very stylish methods with other libraries or some, being realistic and in a productive thinking, is so easy to mantain, and understand, so, you can get other languages trough non programing skills dudes.

I consider better that the arrays method, as you don't need to distribute that big multilanguage to get some traductions, besides, you are using less memory as you are loading only the needed traducted strings.

About the thing that, those can't be redefined; well, generrally if you are defining constants is because they don't need or must not be changed, so if needed that you can use a section for defined strings and variable ones.

I addmint that i don't like the way those define files looks but, take in count that your visitors are not worried at all about the way idiom is implemented, is a perfect valid implementations and you'll can easily get some traductions.