Questions Updated instead of making a new question...
I really want to provide a few alternative languages other then English on my social network site I am building, this will be my first time doing any kind of language translation so please bear with me.
I am researching so I am al ear and open to ideas and I have a lot already here is are the questions.
1)
What does i18n mean, I see it often when researching language translation on SO?
2)
Most people say use gettext PHP has an extension or support for it,
well I have been researching it and I have a basic understanding of it, as far as I can tell it is a lot of extra work to go this route,
I mean coding my site to use it's functions ie; _('hello world i'm in English for now') or else gettext('hello world i'm in English for now') is no problem as any route I go will require that.
But then you have to install gettext on your server and get it working,
then use some special editors to create special files and compile them I think?
Sounds like a pain, I understand this is supposed to be the best route to go though, well everyone seems to say it is.
So can someone tell me why this is the route to go?
3)
I really like the simplicity of this approach, just building a language array and calling the phrase you need in a function like the example below
, you would then just include a file with the appropriate language array.
What I really want to know is, would this be the less better performance method on a high traffic and fairly large site compared to using gettext and if so can you explain why please?
<?PHP
//Have seperate language files for each language I add, this would be english file
function lang($phrase){
static $lang = array(
'NO_PHOTO' => 'No photo\'s available',
'NEW_MEMBER' => 'This user is new'
);
return $lang[$phrase];
}
//Then in application where there is text from the site and not from users I would do something like this
echo lang('NO_PHOTO'); // No photo's available would show here
?>
* some code used from brianreavis's answer below
It'd probably be best to define a function that handles your language mapping. That way, if you do want to change how it works later, you're not forced to scour hundreds of scripts for cases where you used $lang[...] and replace them with something else.
Something like this would work and would be nice & fast:
function lang($phrase){
static $lang = array(
'NO_PHOTO' => 'No photo\'s available',
'NEW_MEMBER' => 'This user is new'
);
return $lang[$phrase];
}
Make sure the array is declared static inside the function so it doesn't get reallocated each time the function is called. This is especially important when $lang is really large.
To use it:
echo lang('NO_PHOTO');
For handling multiple languages, just have this function defined in multiple files (like en.php, fr.php, etc) and require() the appropriate one for the user.
This might work better:
function _L($phrase){
static $_L = array(
'NO_PHOTO' => 'No photo\'s available',
'NEW_MEMBER' => 'This user is new'
);
return (!array_key_exists($phrase,$_L)) ? $phrase : $_L[$phrase];
}
Thats what i use for now. If the language is not found, it will return the phrase, instead of an error.
You should note that an array can contain no more than ~65500 items. Should be enough but well, just saying.
Here's some code that i use to check for the user's language:
<?php
function setSessionLanguageToDefault() {
$ip=$_SERVER['REMOTE_ADDR'];
$url='http://api.hostip.info/get_html.php?ip='.$ip;
$data=file_get_contents($url);
$s=explode (':',$data);
$s2=explode('(',$s[1]);
$country=str_replace(')','',substr($s2[1], 0, 3));
if ($country=='us') {
$country='en';
}
$country=strtolower(ereg_replace("[^A-Za-z0-9]", "", $country ));
$_SESSION["_LANGUAGE"]=$country;
}
if (!isset($_SESSION["_LANGUAGE"])) {
setSessionLanguageToDefault();
}
if (file_exists(APP_DIR.'/language/'.$_SESSION["_LANGUAGE"].'.php')) {
include(APP_DIR.'/language/'.$_SESSION["_LANGUAGE"].'.php');
} else {
include(APP_DIR.'/language/'.DEFAULT_LANG.'.php');
}
?>
Its not done yet, but well i think this might help a lot.
Don't write your own language framework. Use gettext. PHP has standard bindings that you can install.
Don't reinvent the wheel. Use for example gettext or Zend_Translate.
As the other answers don't really answer all the questions, I will go for that in my answer plus offering a sensible alternative.
1)
I18n is short for Internationalization and has some similarities to I-eighteen-n.
2)
In my honest opinion gettext is a waste of time.
3)
Your approach looks good. What you should look for are language variables. The WoltLab Community Framework 2.0 implements a two-way language system. For once there are language variables that are saved in database and inside a template one only uses the name of the variable which will then be replaced with the content of the variable in the current language (if available). The second part of the system provides a way to save user generated content in multiple languages (input in multiple languages required).
Basically you have the interface text that is defined by the developer and the content that is defined by the user. The multilingual text of the content is saved in language variables and the name of the language variable is then used as value for the text field in the specific content table (as single-language contents are also possible).
The structure of the WCF is sadly in a way that reusing code outside of the framework is very difficult but you can use it as inspiration. The scope of the system depends solely on what you want to achieve with your site. If it is going to be big than you should definitely take a look at the WCF system. If it's small a few dedicated language files (de.php, en.php, etc), from which the correct one for the current language is included, will do.
why not you just make it as multi-dimesional array...such as this
<?php
$lang = array(
'EN'=> array(
'NO_PHOTO'=>'No photo\'s avaiable',
'NEW_MEMBER'=>'This user is new',
),
'MY'=> array(
'NO_PHOTO'=>'Tiada gambar',
'NEW_MEMBER'=>'Ini adalah pengguna baru',
)
);
?>
You can do this:
class T {
const language = "English";
const home = "Home";
const blog = "Blog";
const forum = "Forum";
const contact = "Support";
}
You would have a file like this for each language. To use the text:
There is no place like <?=T::home?>.
The downside is that if you add a new constant, you have to do it for every langauge file. If you forget one, your page breaks for that language. That is a bit nasty, but it is efficient since it doesn't need to create a large associative array and possibly the values even get inlined.
Maybe access could be improved, eg:
class T {
const home = "home";
public static function _ ($name) {
$value = #constant("self::$name");
return $value ? $value : $name;
}
// Or maybe through an instance:
public function __get ($name) {
$value = #constant("self::$name");
return $value ? $value : $name;
}
}
echo "There is no " . T::_("place") . " like " . T::_("home");
$T = new T();
echo "There is no " . $T->place . " like " . $T->home;
We still avoid the array and rely on constant to do the lookup, which I assume is more expensive than using the constants directly. The up side is the lookup can use a fallback when the key is not found.
An extension to the answers above whom deserve the credit - I'm just posting as maybe this will also be useful to someone else who ends up here.
I personally prefer the key to the array to be the actual phrase in my mother tongue (in my case English) rather than a CONSTANT_VALUE because:
I find it easier to read the code when the text is in my native language rather than having to remember what a CONSTANT_VALUE actually outputs
It means no lookup is needed to return the phrase for visitors who also use my naitive language (giving marginally better performance)
It's one less list of values to maintain
The downside is that it's harder to spot missing values in other languages as you don't necessarily have a master list anymore - I also log a warning from the abstract method so that I spot any missing values.
I implemented as:
An abstract class with static methods for outputting the text value (using late static binding)
A concrete class for each language: English overriding the method to return the phrase without translation, other languages overriding the list of phrases so that a translated phrase is returned
<?php
namespace Language;
abstract class _Language
{
protected static $displayText = array();
public static function output($phrase){
return static::$displayText[$phrase] ?? $phrase;
}
}
<?php
namespace Language;
class English extends _Language
{
public static function output($phrase){
return $phrase;
}
}
<?php
namespace Language;
class Spanish extends _Language
{
protected static $displayText = array(
'Forename' => 'Nombre',
'Registered Email' => 'Correo electrónico registrado',
'Surname' => 'Apellido'
);
}
Usage:
$language = new \Language\Spanish();
echo $language::output('Forename'); // Outputs: Nombre
$language = new \Language\English();
echo $language::output('Registered Email'); // Outputs: Registered Email
Unfortunately gettext not work good and have problems in various situation like on different OS (Windows or Linux) and make it work is very difficult.
In addition it require you set lot's of environment variables and domains and this not have any sense.
If a developer want simply get the translation of a text he should only set the .mo file path and get the translation with one function like translate("hello","en_EN"); With gettext this is not possible.
Related
Consider writing a PHP library, that will get published through Packagist or Pear. It is addressed to peer developers using it in arbitrary settings.
This library will contain some status messages determined for the client. How do I internationalize this code, so that the developers using the library have the highest possible freedom to plug in their own localization method? I don't want to assume anything, especially not forcing the dev to use gettext.
To work on an example, let's take this class:
class Example {
protected $message = "I'd like to be translated in your client's language.";
public function callMe() {
return $this->message;
}
public function callMeToo($user) {
return sprintf('Hi %s, nice to meet you!', $user);
}
}
There are two problems here: How do I mark the private $message for translation, and how do I allow the developer to localize the string inside callMeToo()?
One (highly inconvenient) option would be, to ask for some i18n method in the constructor, like so:
public function __construct($i18n) {
$this->i18n = $i18n;
$this->message = $this->i18n($this->message);
}
public function callMeToo($user) {
return sprintf($this->i18n('Hi %s, nice to meet you!'), $user);
}
but I dearly hope for a more elegant solution.
Edit 1: Apart from simple string substitution the field of i18n is a wide one. The premise is, that I don't want to pack any i18n solution with my library or force the user to choose one specifically to cater for my code.
Then, how can I structure my code to allow best and most flexible localization for different aspects: string translation, number and currency formatting, dates and times, ...? Assume one or the other appears as output from my library. At which position or interface can the consuming developer plug in her localization solution?
The most often used solution is a strings file. E.g. like following:
# library
class Foo {
public function __construct($lang = 'en') {
$this->strings = require('path/to/langfile.' . $lang . '.php');
$this->message = $this->strings['callMeToo'];
}
public function callMeToo($user) {
return sprintf($this->strings['callMeToo'], $user);
}
}
# strings file
return Array(
'callMeToo' => 'Hi %s, nice to meet you!'
);
You can, to avoid the $this->message assignment, also work with magic getters:
# library again
class Foo {
# … code from above
function __get($name) {
if(!empty($this->strings[$name])) {
return $this->strings[$name];
}
return null;
}
}
You can even add a loadStrings method which takes an array of strings from the user and merge it with your internal strings table.
Edit 1: To achieve more flexibility I would change the above approach a little bit. I would add a translation function as object attribute and always call this when I want to localize a string. The default function just looks up the string in the strings table and returns the value itself if it can't find a localized string, just like gettext. The developer using your library could then change the function to his own provided to do a completely different approach of localization.
Date localization is not a problem. Setting the locale is a matter of the software your library is used in. The format itself is a localized string, e.g. $this->translate('%Y-%m-%d') would return a localized version of the date format string.
Number localization is done by setting the right locale and using functions like sprintf().
Currency localization is a problem, though. I think the best approach would be to add a currency translation function (and, maybe for better flexibility, another number formatting function, too) which a developer could overwrite if he wants to change the currency format. Alternatively you could implement format strings for currencies, too. For example %CUR %.02f – in this example you would replace %CUR with the currency symbol. Currency symbols itself are localized strings, too.
Edit 2: If you don't want to use setlocale you have to do a lot of work… basically you have to rewrite strftime() and sprintf() to achieve localized dates and numbers. Of course possible, but a lot of work.
There's a main problem here. You don't want to make the code as it is right now in your question for internationalization.
Let me explain. The main translator is probably a programmer. The second and third might be, but then you want to translate it to any language, even for non-programmers. This ought to be easy for non-programmers. Hunting through classes, functions, etc for non-programmers is definitely not okay.
So I propose this: keep your source sentences (english) in an agnostic format, that it's easy to understand for everyone. This might be an xml file, a database or any other form you see it fits. Then use your translations where you need them. You can do it like:
class Example {
// Fetch them as you prefer and store them in $messages.
protected $messages = array(
'en' => array(
"message" => "I'd like to be translated in your client's language.",
"greeting" => "Hi %s, nice to meet you!"
)
);
public function __construct($lang = 'en') {
$this->lang = $lang;
}
protected function get($key, $args = null) {
// Store the string
$message = $this->messages[$this->lang][$key];
if ($args == null)
return $this->translator($message);
else {
$string = $this->translator($message);
// Merge the two arrays so they can be passed as values
$sprintf_args = array_merge(array($string), $args);
return call_user_func_array('sprintf', $sprintf_args);
}
}
public function callMe() {
return $this->get("message");
}
public function callMeToo($user) {
return $this->get("greeting", $user);
}
}
Furthermore, if you want to use a small translation script I did, you can simplify it furthermore. It uses a database, so it might not have so much flexibility as you're looking for. You need to inject it and the language is set in the initialization. Note that the text is automatically added to database if not present.
class Example {
protected $translator;
// Translator already knows the language to translate the text to
public function __construct($Translator) {
$this->translator = $Translator;
}
public function callMe() {
return $this->translator("I'd like to be translated in your client's language.");
}
public function callMeToo($user) {
return sprintf($this->translator("Hi %s, nice to meet you!"), $user));
}
}
It could be easily modified to use a xml file or any other source for translated strings.
Notes for the second method:
This is different than your proposed solution since it is doing the work in the output, rather than in the initialization, so no need to keep track of every string.
You only need to write your sentences once, in English. The class I wrote will put it in the database provided it's correctly initialized, making your code extremely DRY. That's exactly why I started it, instead of just using gettext (and the ridiculous size of gettext for my simple requirements).
Con: it's an old class. I didn't know a lot back then. Now I'd change a couple of things: making a language field, rather than en, es, etc, throwing some exceptions here and there and uploading some of the tests I did.
The basic approach is to provide the consumer with some method to define a mapping. It can take any form, as long as the user can define a bijective mapping.
For example, Mantis Bug Tracker uses a simple globals file:
<?php
require_once "strings_$language.txt";
echo $s_actiongroup_menu_move;
Their method is basic but works just fine. Wrap it in a class if you prefer:
<?php
$translator = new Translator(Translator::ENGLISH); // or make it a singleton
echo $translator->translate('actiongroup_menu_move');
Use an XML file instead, or an INI file, or a CSV file... whatever format of your liking, in fact.
Answering your later edits/comments
Yes, the above does not differ much from other solutions. But I believe there is little else to be said:
translation can only be achieved through string substitution (the mapping may take an infinite number of forms)
formatting number and dates is none of your concern. It is the presentation layer's responsibility, and you should just return raw numbers (or DateTimes or timestamps), (unless your library's very purpose is localisation ;)
I have a code base that can be customized for different customers. This is handled, in most instances, by loading a class with constants specific to each customer, but with the same class name. Class name collision is managed, and, obviously, only one is loaded at a time.
class Customer () {
const CUST_NAME = 'Alpha Corp.';
}
class Customer () {
const CUST_NAME = 'Beta, Inc.';
}
For each customer, I also want the code to be internationalized. So, a program can draw its displayed text from a language-specific source, but also still be dynamic with regard to customer-specific information. Assume the customer-specific information is indifferent toward i18n. This reduces the number of files from one per customer per language, to just one per language. The desired result is to implement something similar to
// in separate language files:
$greeting = 'Hello. Welcome to ';
$greeting = 'Hola. Bienvenido a ';
// in front-end
$greeting = get $greeting in desired language;
echo $greeting, Customer::CUST_NAME, "\n";
The best solution I can devise to meet these requirements is an i18n class (or family of classes), which would handle multiple customers and/or multiple languages. However, it is execution-expensive to call methods, as opposed to string literals or constants. The methods would combine translations from constants or external sources, and combine them with values from the Customer class. (I am stuck with PHP 5.2 for now, so the niceties of heredoc for class properties/constants is unavailable.)
class i18n_en () {
const GREETING = 'Hello. Welcome to ';
static public function getGreeting () {
return self::GREETING . Customer::CUST_NAME;
}
}
Alternatively, I can write a script for a "template approach", in which I maintain a template file of the class, with text placeholders. The customer-specific files are generated at the time of customer creation from these templates. It would be easy to generate or re-generate files as needed, but I'm back to needing a separate file for each customer for each language. Thus, it doesn't meet my need as nicely.
Surely, I'm not the first to face this problem. Can anyone offer alternatives or best practices? Since the code will execute many times more than I'll create a new customer, I prefer run-time efficiency to ease of maintenance. Of course, a great solution will offer both.
There are two tried and true methods for PHP internationalization.
The most prevalent is to create huge language arrays like so:
define('MSG__GREETING', 1);
$lang['en_US'] = array(MSG__GREETING => 'Hello, and welcome to ');
$lang['fr_FR'] = array(MSG__GREETING => 'Bonjour, et bienvenue à ');
$selectedLang = 'fr_FR';
echo $lang[$selectedLang][MSG__GREETING] . 'Fhloston Paradise';
Unfortunately, this gets very tedious very quickly.
The ideal method, which I've used numerous times, is the accepted method of internationalization for Linux apps: il8n, via PHP's gettext extension.
With this method, you basically end up doing stuff like this:
setenv('LANG=fr_FR');
echo _('Hello, and welcome to ') . 'Fhloston Paradise';
and then in il8n files (called .po) you have each translation. It's actually much easier to maintain and extend over the long run, especially since you can just email your .po files to various translators and they just fill in the blanks, as it were... no coding skills necessary.
Here's a tutorial to get you started: http://kunststube.net/frontback/
Personally, when handling internationalization I have always used templates, specifically Smarty - http://www.smarty.net/ and create language based template. You can also use translation keys in a database, ex table with key, language, translation columns which can be cached this way you aren't calling tons of methods. Also, you can cache the output of all the translation methods in memory using memcached. Tons of ways. Hope some of these help a little.
For multi-language usage of CMS, they translate terms by a function similar to
function __($word) {
include 'fr.php';
if(!empty($lang[$word])) {$translated=$lang[$word];
} else {
$translated = $word;
}
return $translated;
}
Since we need to use this function several times in a php page, as all words and phrases will be echoed by __(' '); does the function need to include the language time every time, or it will be cached for the function after first load?
Since the language file contains a complete list of words and phrased used throughout the website (thousands of key/value), pho needs to load this long array into memory every time a page is visited. Is it really the best approach to add multi-language feature to a CMS?
If you can't use gettext() for some reason, you'd be better off, with something like this, to put it into an object with the included language strings as a static array, something like:
class Message {
private static $_messages = array();
public static function setMessageLibrary($sMessageLibrary) {
require_once $sMessageLibrary;
self::$_messages = $aMsgs;
}
public static function getMessage($sMessageId) {
return isset(self::$_messages[$sMessageId]) ? self::$_messages[$sMessageId] : "";
}
}
Your message library file (included with the setMessageLibrary() static function), of which you'll have one per language, will need a variable in it called $aMsgs which might look something like:
// Messages for fr-FR
$aMsgs = array(
'hello_everybody' => "Bonjour tout le monde"
...
and so on
);
Since it's all static but within the object you can effectively cache that included language file by setting it at the start of your script.
<?php
Message::setMessageLibrary('/lang/fr-FR/messages.inc.php');
echo Message::getMessage('hello_world');
echo Message::getMessage('another_message');
echo Message::getMessage('yet_another_message');
?>
All three messages will then reference the single language array stored in Message::$_messages
There's no sanitisation, nor sanity checks in there, but that's the basic principle anyway ... if you can't use gettext() ;)
1) it won't be cached, use include_once instead
2) no, i think gettext is doing it another/better way
IIRC, it will do some caching.
No, it's not. Check out gettext.
I am doing a PHP web site, without using any framework. I need that the site is available in several languages, and I was reading about it and it seems to be a little bit confusing. There are several solutions but all seem to depend on a specific framework.
What you think of using a simple translation function like the one shown below?
I mean, I would like to know what can be a disadvantage of using such code.
Here it is (this is just a simple and incomplete sample):
class Translator{
private $translations;
public function __construct(){
$this->translations = array(
'Inbox' => array(
'en' => 'Inbox',
'fr' => 'the french word for this'
),
'Messages' => array(
'en' => 'Messages',
'fr' => 'the french word for this'
)
//And so on...
);
}
public function translate($word,$lang){
echo $this->translations[$word][$lang];
}
}
It does not look bad. I've seen this used many times.
I would however separate the different strings in one file per language. At least, or if the files get large, one file per module per language.
Then your translation class can load and cache the language files (if you don't rely on any other caching system) every time a new language is to be used.
A little example of what i mean
class Translator {
private $lang = array();
private function findString($str,$lang) {
if (array_key_exists($str, $this->lang[$lang])) {
return $this->lang[$lang][$str];
}
return $str;
}
private function splitStrings($str) {
return explode('=',trim($str));
}
public function __($str,$lang) {
if (!array_key_exists($lang, $this->lang)) {
if (file_exists($lang.'.txt')) {
$strings = array_map(array($this,'splitStrings'),file($lang.'.txt'));
foreach ($strings as $k => $v) {
$this->lang[$lang][$v[0]] = $v[1];
}
return $this->findString($str, $lang);
}
else {
return $str;
}
}
else {
return $this->findString($str, $lang);
}
}
}
This will look for .txt files named after the language having entries such as this
Foo=FOO
Bar=BAR
It always falls back to the original string in case it does not find any translation.
It's a very simple example. But there is nothing wrong in my opinion with doing this by yourself if you have no need for a bigger framework.
To use it in a much simpler way you can always do this and create a file called 'EN_Example.txt'
class Example extends Translator {
private $lang = 'EN';
private $package = 'Example';
public function __($str) {
return parent::__($str, $this->lang . '_' . $this->package);
}
}
Sometimes you wish to translate strings that contain variables. One such approach is this which i find simple enough to use from time to time.
// Translate string "Fox=FOX %s %s"
$e = new Example();
// Translated string with substituted arguments
$s = printf($e->__('Fox'),'arg 1','arg 2');
To further integrate variable substitution the printf functionality can be put inside the __() function like this
public function __() {
if (func_num_args() < 1) {
return false;
}
$args = func_get_args();
$str = array_shift($args);
if (count($args)) {
return vsprintf(parent::__($str, $this->lang . '_' . $this->package),$args);
}
else {
return parent::__($str, $this->lang . '_' . $this->package);
}
}
There are a few things it appears you haven't considered:
Are you simply translating single words? What about sentence structure and syntax that differs between languages?
What do you do when a word or sentence hasn't been translated into a language yet?
Does your translations support variables? The order of words in a sentence can differ in different languages, and if you have a variable it usually won't be good enough simply to split the word around the sentence.
There are a two solutions that I've used and would recommend for PHP:
gettext - well supported in multiple languages
intsmarty - based on Smarty templates
The advantage with using a class or functions for this is that you can change the storage of the languages as the project grows. If you only have a few strings, there is absolutely no problems with your solution.
If you have a lot of strings it could take time, memory and harddrive resources to load the language arrays on all page loads. Then you probably want to split it up to different files, or maybe even use a database backend. If using i database, consider using caching (for example memcached) so you don't need to query the database hundreds of times with every page load.
You can also check out gettext which uses precompiled language files which are really fast.
I'd have thought it might be easier to simply use an include for each language, the contents of which could simply be a list of defines.
By doing this, you'd avoid both the overhead of including all the language data and the overhead of calling your 'translate' function on a regular basis.
Then again, this approach will limit things in terms of future flexability. (This may not be a factor though.)
It's fine to not use a framework. The only problem I see with your function is that it's loading a lot of data into memory. I would recommend having arrays for each language, that way you would only need to load the language that is being used.
Is using constants (defines) a bad practice?
That's how I have it setup. It was just to have multi langua support.
I have one portuguese file and an english files filled with:
define('CONST','Meaning');
Maybe this is a bit a memory hog, but I can access from every where I want :)
I may change to a oop approach, but for now I have this.
When I had a problem like this (but for a very small site, just a few pages) a long time ago, I created a file named langpack.php and any string of text on my site had to be run through that. Now, I would use a similar approach, but split over multiple files.
Example OOP Approach
langpack.php
abstract class langpack {
public static $language = array();
public static function get($n) {
return isset(self::$language[$n]) ? self::$language[$n] : null;
}
}
english.php
final class English extends langpack {
public static $language = array(
'inbox' => 'Inbox',
'messages' => 'Messages',
'downloadError' => 'There was an error downloading your files',
);
}
french.php
final class French extends langpack {
public static $language = array(
'inbox' => 'Inbioux',
'messages' => 'Omelette du Fromage',
'downloadError' => 'C\'est la vie',
);
}
You should get the idea from there. Implement an autoloader in a config file and then loading the language should be something you could easily do from the session, URL, or whatever, by using PHP's variable nature in conjunction with class instantiation, something like this:
$langpack = new $_SESSION['language'];
echo $langpack::get('inbox');
Of course, all this could be done with simple arrays, and accessed in an imperative style (with absolute references handled via $GLOBALS) to reduce some overhead and perhaps even make the mechanisms by which this is all handled a bit more transparent, but hey, that wouldn't be very OO, would it?
One could also be using the Symfony translation component, no framework is required and composer helps dealing with dependencies:
composer install --prefer-dist "symfony/translation":"#stable"
I think that's ok if you're not using any framework for other reasons. We've been in the same scenario as yours, when you cannot/don't want to use a more structured translation framework:
We were working at a small PHP project and where looking for some simple translation mechanism. We used an array approach similar to yours, but with separate files for each language texts. We put up a small component to keep thins as clean as possible.
If you want to give a look, we shared that on https://github.com/BrainCrumbz/simple-php-translate. Please feel free to improve it!
i would simply use a function with controller and language inputs, for instance:
function getLanguageFile($controller, $lang){
// security: only allow letters a-z in both strings
$controller = preg_replace('/([^a-z]*)/', '', $controller);
$lang = preg_replace('/([^a-z]*)/', '', $lang);
// return language details if present on disk
if (is_file('lang/'.$controller.'/'.$lang.'.json')){
return json_decode(file_get_contents('lang/'.$controller.'/'.$lang.'.json'));
}
return false;
}
you simply have to place your json formatted strings in lang/index/en.json if controller is index and language is en.
you could add a function for dependencies (for instance you want to load index controller values on access to another controller) all you have to do is to merge the results.
you could simply include php files with arrays aswell and just return the array then, but i suggest you split these translations in larger projects. if your project isn't that big, your function is absolutely ok.
I'm using CodeIgniter, and will likely use their template library as I want to keep things extremely simple to use. The content for the template variables will come from the database, but I want the business admins to know what content areas are available. Basically the names of the parameters when they choose a specific template. For instance, Joomla uses an extra XML file that defines each area, whereas Wordpress uses comments within a page template to inform the system that the PHP file is a template. I like the Joomla approach because you don't have to parse the PHP file to find the areas, but I like the Wordpress approach because you don't have an extra XML file associated with every template. Are there other approaches that I'm missing?
I think the nicest way would be to add a small hack to the template parser class. The code looks quite readable and clean in system/libraries/Parser.php. You could insert a hook in that class that can be used to keep track of the variables. I don't know, if it works, but here's a snippet:
class CI_Parser {
var $varCallback;
function setVarCallback($callbackFunction) {
$this->varCallback = $callbackFunction;
}
...
function _parse_single(...) {
$callback = $this->varCallback;
$callback($key);
}
...
//Somewhere in your code
function storeVarName($variableName) {
// Persist the variable name wherever you want here
}
$this->parser->setVarCallback('storeVarName');
You could do this directly in the controller:
// in the controller
print_r($data);
$this->load->view("main", $data);
Or a little more rudimentary, but you could pass to the template a PHP array of variables (or an object):
// in the controller
$data = array();
$data["namespace"] = array(
"title" => "My website",
"posts" => array("hi", "something else")
);
$this->load->view("main", $data);
And then in the view, have a flag to print_r the namespace to show all the vars available, so that business admins know exactly what to use.
// in the view
if(isset($namespace["showAllVars"])) print_r($namespace);
One option would be to call token_get_all on the PHP file (only when your business admins are loading it up), and parse the output of that.
The best approach, in my opinion, is to keep the variable definitions in another place (such as a database table, or a separate file). This will help with testing (i.e., a programmer can't just remove a tag and it's gone) and making sure things are still working as you move on with the application development in time.
Another advantage is that your application logic will be independent from the templating engine.
On a side note, if you expect a lot of traffic, you may consider using smarty instead. We have done extensive testing with most of the templating engines around and smarty is the fastest.