Best way to internationalize simple PHP website - php

I have to develop a pretty simple php website so I don't need framework.
But it's must support multi language (EN/FR/CHINESE).
I have looked for php built in system and I found two ways :
intl module from php5.3 (http://php.net/manual/fr/book.intl.php)
gettext (http://php.net/manual/fr/book.gettext.php)
I have no experience in i18n without framework, so any advices about what's the simplest way to support multi language ?
At end I just need a function that search translation into file (one file by language).
EQ :
trans('hello');
=> en.yaml (yaml or not, it's an example)
hello: "Hello world!"
=> fr.yaml
hello: "Bonjour tout le monde !"
And if possible I prefer Pure PHP implementations

Although ext/gettext and ext/intl are both related to i18 (internationalization), gettext deals with translation while intl deals with internationalizing things like number and date display, sorting orders and transliteration. So you'd actually need both for a complete i18-solution. Depending on your needs you may come up with an home-brew solution relying on the extensions mentioned above or your use components provided by some framework:
Translation
Symfony 2 Translation component: https://github.com/symfony/Translation
Zend Framework Zend_Translate
Internationalization
Zend Framework Zend_Locale
If you only need translation and the site is simple enough, perhaps your simple solution (reading a translation configuration file into an PHP array, using a simple function to retrieve a token) might be the easiest.
The most simple solution I can think of is:
$translation = array(
'Hello world!' => array(
'fr' => 'Bonjour tout le monde!',
'de' => 'Hallo Welt!'
)
);
if (!function_exists('gettext')) {
function _($token, $lang = null) {
global $translation;
if ( empty($lang)
|| !array_key_exists($token, $translation)
|| !array_key_exists($lang, $translation[$token])
) {
return $token;
} else {
return $translation[$token][$lang];
}
}
}
echo _('Hello World!');

I know this is an old question, but I feel that the answers are lacking a more hands-on approach from start to finish. This is what I did to get translation working using PHP's gettext library and Poedit without using any additional PHP libraries on a Debian server:
Preparation step 1: Install gettext and the locales on the server
I am not sure how this is done with other operating systems, but for Debian, you do:
sudo apt-get install gettext
sudo dpkg-reconfigure locales
Edit: I assumed Ubuntu would be the same as Debian, but apparently it's slightly different. See this page for instructions for installing locales on Ubuntu.
Make sure you select all of the locales that you want to use. You should then see something like:
Generating locales (this might take a while)...
en_US.UTF-8... done
es_MX.UTF-8... done
fr_FR.UTF-8... done
zh_CN.UTF-8... done
Generation complete.
Note: Make sure you select the right variants and character encodings (most likely UTF-8) for each language. If you install es_MX.UTF-8 and try to use es_ES.UTF-8 or es_MX.ISO-8859-1 it won't work.
Preparation step 2: Install Poedit on the translators' computers
Poedit is available from the software repository for most Linux operating systems. For Debian-based, just execute:
sudo apt-get install poedit
For Windows and Mac, go to: https://poedit.net/download
Start coding:
Ok, now you're ready to get started coding. I wrote the following gettext() wrapper function to translate both singular and plurals:
function __($text, $plural=null, $number=null) {
if (!isset($plural)) {
return _($text);
}
return ngettext($text, $plural, $number);
}
Example usage:
// Singular
echo __('Hello world');
// Plural
$exp = 3;
printf(
__(
'Your account will expire in %d day',
'Your account will expire in %d days',
$exp
),
$exp
);
This will work for all languages, not only languages where plural is anything where n != 1 - this includes languages with multiple plural types.
You can also add translator notes like this:
/** NOTE: The name Coconut Hotel is a brand name and shouldn't be
translated.
*/
echo __('Welcome to Coconut Hotel');
You can change the text from NOTE to whatever you want, but you will have to alter it in the shell script below. Important: The translators note must be part of a comment on the line immediately preceding the __() function or it won't be picked up when we scan the PHP files for translatable strings.
// Warning! THIS WILL NOT WORK!
/* NOTE: This translator's note will not be picked up because it is
not immediately preceding the __() function. */
printf(
__(
'Your account will expire in %d day',
'Your account will expire in %d days',
$exp
),
$exp
);
// Warning! THIS WILL NOT WORK!
After you are ready to send the strings off to the translators, save the following as a shell script (e.g. update.sh) in your application's root directory:
#!/bin/sh
find . -iname "*.php" | xargs xgettext --add-comments=NOTE --keyword=__:1,2 --keyword=__ --from-code=UTF-8 -o i18n.pot
find . -name '*.po' | xargs -I{} msgmerge -U {} i18n.pot
To execute it, just do:
cd /path/to/script && sh update.sh
This will recursively scan for all PHP files in that directory and create a .pot file (I called it i18n.pot, but feel free to name it whatever you like) and update any existing .po files it finds with the new strings.
We then need to create the directories that all the locale files will be stored, one for each locale. They need to be of the format ./locale/{locale}/LC_MESSAGES. For example:
cd /path/to/your/project
mkdir -p ./locale/en_US.UTF-8/LC_MESSAGES
mkdir -p ./locale/es_MX.UTF-8/LC_MESSAGES
# ...etc.
You need to decide on a text domain to use. This can be anything you want, but the script will look for a file called {yourTextDomain}.mo within the LC_MESSAGES folder for that language. Put the following in your PHP script:
define('TEXT_DOMAIN', 'yourdomain');
bindtextdomain(TEXT_DOMAIN, __DIR__.'/locale');
textdomain(TEXT_DOMAIN);
bind_textdomain_codeset(TEXT_DOMAIN, 'UTF-8');
Then to actually switch to another locale, do:
$lang = 'es_MX.UTF-8'; // Change this to the language you want to use
if (setlocale(LC_ALL, $lang) === false) {
throw new Exception("Server error: The $lang locale is not installed. Please update the server's localisations.");
}
putenv('LC_ALL='.$lang);
Initially, you send the .pot file generated by the script above to the translators. They then open Poedit and click on File > New from POT/PO file. When they save it, they need to save it as {yourTextDomain}.po. The {yourTextDomain} needs to be exactly the same as the text domain you have in your PHP script. When they save it, it will automatically create both the .po file and the .mo file. Both of these need to be saved in that language's LC_MESSAGES directory when they are done translating.
Now when you update the strings in your PHP file, just re-execute the shell script and send the newly updated .po files to the translators. They then translate the strings and both the .po and .mo files need to be re-uploaded.
That's it. It may seem slightly difficult to get set up, but once you have it up and running, it's really easy.

Gettext seems to be what you need.
There is a file by langage (except for the original one) and it's very easy to use :
echo _('Bonjour, ça va ?');
will print Hello , how are you ? in english.
There is some tools with gettext that could scan your php file and search for translatable string (in fact all string in _() or gettext()). Thanks to that you don't have to worry about the different langage file. You just code your website in the original langage and the langage file will automatically created later.
Nevertheless gettext is more a translation tools whereas intl is really an i18n one (number formating for example)

Althought you don't need a framework you can use a framework. The internationalization features in Zend Framework is pretty good and you can just use that part of it instead of using all the parts (including MVC)

Related

how to get current locale in PHP

I would like to get the current system locale of a server (say windows 7 os). This is to ensure that different language setting uses different parts of code in PHP.
However, I could not find any API that does this.
Can anyone tell me the name of the function?
Having thought more about the problem and the particular setup I have, I came up with this solution, which seems to work. Note that I don't have control over what languages I need to support: there are translation files dropped into a predefined place and system locales installed by someone else. During runtime, I need to support a particular language if corresponding translation file exists and and system locale is installed. This got me to this solution:
Use below function
function getLocale($lang)
{
$locs = array();
exec('locale -a', $locs);
$locale = 'en-IN';
foreach($locs as $l)
{
$regex = "/$lang\_[A-Z]{2}$/";
if(preg_match($regex, $l) && file_exists(TRANSROOT . "/$lang.php"))
{
$locale = $l;
break;
}
}
return $locale;
}
I'm defaulting to en-IN if I cannot resolve a locale, because I know for certain that en-IN is installed.
Oddly, you get the locale for the PHP process by using the setlocale function like so:
setlocale (LC_ALL,"0");
The second parameter is "locales" and it says in the docs:
If locales is "0", the locale setting is not affected, only the current setting is returned.
So it always returns the locale, but you can tell it to set nothing and just return the current locale.
Note: This isn't the system locale, but the locale of the PHP process itself, which is usually derived from the system locale, but may be different if you previously called setlocale and changed it. I thought at first that that's what this question was looking for, but I think the answer from Gags is more correct, calling "locale -a" with "exec()" to get the actual system language independent of the PHP process.
Best answer above from Gags.
If you want the contents of the accept-language: header from the current request, if there is one, use:
$_SERVER['HTTP_ACCEPT_LANGUAGE']

cakephp .po files not being used

I'm trying to get CakePHP's i18n component to work. I have extracted my strings to app/Locale/default.pot using the i18n console task. I then copied it into app/Locale/eng/LC_MESSAGES/default.po and app/Locale/fra/LC_MESSAGES/default.po making sure to change the extension. I used the program Virtaal (similar to Poedit) to translate some of the strings.
In my app/Config/core.php I have set my default language to english with Configure::write('Config.language', 'eng'); if I change it to Configure::write('Config.language', 'fra'); I expected to see the new translated strings but nothing changed. I tried setting the Config.language key in the session as well but it didn't do anything. Printing out the configure value and session values I can see they are being set.
Am I missing something here? also in the many different posts I've been reading about i18n in CakePHP I've seen the key fre being used interchangeably with fra is there a difference?
After http://book.cakephp.org/1.3/en/The-Manual/Common-Tasks-With-CakePHP/Internationalization-Localization.html it should be fre for french.
// locale path
/app/locale/fre/LC_MESSAGES/default.po (French)
// To change or set the language for your application, all you need to do is the following:
Configure::write('Config.language', 'fre');
// To set the language for the current user, store the setting in the Session object, like this:
$this->Session->write('Config.language', 'fre');
Further than that:
In our cakePHP application I have to restart the apache webserver after changing the files to get the new strings because of caching. Perhaps you have to do that too, but I'm not quite sure as we generate .mo files out of the .po via an POEdit setting. I dont't think you are forced to do this, because the cookboke doesn't say anything about that (or I didn't found it now :D ).
edit:
Looks like cake is using the bibliographic codes for french but the terminology for german. That's very confusing :/ : http://www.loc.gov/standards/iso639-2/php/code_list.php

How do I create ICU resource files for use in PHP?

I'm following the instructions from BryanH's answer here: gettext() equivalent in Intl library? and trying to implement localization (translation) with php-intl, but I keep getting the same problem this person had: ResourceBundle returns NULL without any errors being raised
He mentions he created the dat files with a tool (which I cannot figure out how to work) while the person in the former answer simply appears to be using txt files with a .res extension.
How do I properly implement localization with php-intl and ResourceBundle, and what am I doing wrong?
The goal is to have various data files with different languages so I can do something similar to
$t = new Translator();
$t->setResource(new \ResourceBundle('es', 'locales_folder/'));
$t->echo("somestring"); // "el stringo"
..much like the person in the first answer had. Also, the aim is to have easily editable files, so I can give them to translators for fixes, updates, and so on. I realize I could easily do this with custom solution through a simple text file which gets parsed and saved into memcache on first request, where it then persists and gets served from without having to re-read the .dat files, but I'd rather take the suggested route here.
Edit: Just to get it out there - I implemented the same thing with gettext successfully and it was dead easy - save for one bug that persists across linux systems (http://www.php.net/manual/en/book.gettext.php#91187) - but I'd like to rely on the more modern and all-inclusive intl extension if possible.
I can provide a solution to your question on how to create and use .res files for the intl PHP extension, however I have no experience with speed and use in production systems, you will have to see for yourself if this can be a replacement for gettext.
In my opinion a great benefit of gettext are additional tools such as xgettext which can extract strings to be translated. Still, using resources might be more useful in your use-case.
To generate a .res file you need to use the program genrb which is bundled with ICU. (for example when installing ICU on OSX using brew install icu4c (see this tutorial) you can find the tool at /usr/local/Cellar/icu4c/*/bin/genrb (replace * with the version number))
Next you prepare a file for each locale. Let's do this for two languages in this example, German and Spanish.
$ cat de.txt
de {
hello { "Hallo" }
}
$ cat es.txt
es {
hello { "Hola" }
}
$ genrb *.txt
genrb number of files: 2
You now have 2 additional files de.res and es.res, which you can now access in PHP
<?php
foreach (array("de", "es") as $locale) {
$r = ResourceBundle::create($locale, __DIR__);
echo $r["hello"], "\n";
}
which will output
Hallo
Hola
So in essence, you can hand those *.txt files to your translaters and prepare them for PHP using genrb.
Finally, a small example for the class you were trying to write:
<?php
class Translator {
private $resourceBundle = null;
public function __construct(\ResourceBundle $r) {
$this->resourceBundle = $r;
}
public function __get($string) {
return $this->resourceBundle[$string];
}
}
$t = new Translator(new \ResourceBundle('es', __DIR__));
echo $t->hello;

Working with CodeKit and haml/PHP

in my latests projects I have use Rails. Now I have to do something in PHP (which I actually hate, or love too much Ruby syntax).
So now I am very used to work with Sass and haml, which I also love. So I bought CodeKit for doing things outside rails framework.
Wanted just to know if there is any option for use haml in PHP files, or PHP in haml files, and that the file compiles with CodeKit, even having PHP.
I know this is a very late reply, just found this question when searching myself.
In addition to Kevin's reply above:
You can automatically convert the html to php by simply setting the output path for that particular file, and then specifying the extension.
Right click your HAML or HTML
"Set output path..."
"Output filename and extension"
Change it from index.html or index.haml to index.php.
You can use the :plain filter to preserve the php, it does not parse the filtered text. This is useful when you need to keep multiple lines of php.
ex:
:plain
<?php foreach ($es as $e) {
echo $e;
} ?>
I have not heard about any haml/php color syntax for Sublime Text 2 yet.
There is another setting that can be useful for working with php : check the don't escape HTML character in the HAML config on Codekit : http://d.pr/i/4pmv
This will manage with that
%form#booking_log{:name => "booking_log", :method => "post", :action => "<?=$this->action('bookingGetLog')?>"}

Vim PHP omni completion

I'm trying to get PHP autocompletion right in Vim. Right now when I do a $blog = new Blog(); $blog-> and then hit CTRL+X CTRL+O I'd expect omnicompletion to return all the functions in the class Blog.
Instead, it returns all functions for the entire project. I've built ctags for my project like so: ctags -R *
Is there any way to make the autocompletion context-aware?
catchmeifyoutry's answer points out a work-around by adding a comment such as /* #var $myVar myClass */ immediately before the line on which you use omnicomplete, however this is cumbersome and for the time it takes to write the comment, you may as well have written the function name yourself.
Solution: phpComplete
It is a Vim script: phpComplete
You will still need a tags file generated for your classes, but you can then use omni complete within the file, like so (modified from the description on the script's page);
This patch allows for in-file checking so you don't need the comment.
$blog = new Blog;
...
$blog->Blah(); // <-- complete without comment
It also allows support for singleton instantiations:
$instance = Class::getInstance();
$instance->completeMe(); // sweet completion
" Assuming Vim 7 (full version) is installed,
" adding the following to your ~/.vimrc should work.
filetype plugin on
au FileType php set omnifunc=phpcomplete#CompletePHP
" You might also find this useful
" PHP Generated Code Highlights (HTML & SQL)
let php_sql_query=1
let php_htmlInStrings=1
" Hope this helps!
(via http://www.linuxquestions.org/questions/linux-software-2/vim-omin-completion-for-php-621940/#post3155311)
Omnicompletion will only work if the tag file contains both the class definition, and the variable declaration.
Straightforward solution
In general that means that you will need to save and (re)generate the tags file after the class Blog { ... } and $blog = new Blog(); parts, but before trying $blog-> <C-X><C-O>.
This is because the PHP omni-complete function will look for the class declaration of the $blog variable in the tags file.
(BTW if you have opened the tags file in a buffer, reload it after regenerating it.)
Alternative
The vim documentation (:help ft-php-omni) also defines a way which doesn't require the variable to be indexed in the tags file, but uses instead a specific comment on the preceding line:
/* #var $myVar myClass */
$myVar->
However, the class definition still does need to be in the tag file, and the comment is needed every time you want to use omni-complete. So typing away in a new PHP file still won't give you nice omni-completion :(
Just a thought
Maybe it is possible to generate on-the-fly a temporary tags file (like the taglist plugin) of just the unsaved buffer, and allow omni-complete to use it too?? I'm not a great vim hacker though...
This one works as expected:
https://github.com/shawncplus/phpcomplete.vim
I am just missing the function parameters in the pveview!
The following works better. Taken from http://weierophinney.net/matthew/archives/134-exuberant-ctags-with-PHP-in-Vim.html
ctags \
-f ~/.vim/tags \
-h ".php" -R \
--exclude="\.svn" \
--totals=yes \
--tag-relative=yes \
--PHP-kinds=+ivcf \
--regex-PHP='/(abstract)?\s+class\s+([^ ]+)/\2/c/' \
--regex-PHP='/(static|abstract|public|protected|private)\s+function\s+(\&\s+)?([^ (]+)/\3/f/' \
--regex-PHP='/interface\s+([^ ]+)/\1/i/' \
--regex-PHP='/\$([a-zA-Z_][a-zA-Z0-9_]*)/\1/v/' \
Even with the above, there seems to be some issues. e.g. phpcomplete doesn't seem to support methods of instance vars.
$this->objA = new SomeClass();
$this->objA-><do_autocomplete> #fails
However,
$objA = new SomeClass();
$objA-><do_autocomplete> #works
After trying to get phpcomplete working for the last few hours my advice to anyone also trying, is to STOP. It doesn't work well and is not worth the trouble.
In C++, I run the following to get better context sensitivity:
ctags '--c++-kinds=+p' '--fields=+iaS' '--extra=+q'
It's not perfect, but after ctags adds the extra information to the tags file as specified by the above command vim handles completion better.
You can use a pretty powerful combo:
Phpactor
nvim-completion-manager
I tried a lot of stuff: PHPComplete, Padawan and so on. This is the best I could find.
In case you are interested, I wrote as well an article how to do a PHP IDE with Vim.
I've created a vim plugin for my padawan.php completion server. Checkout this video to see how it works.
try
curl -L -s https://git.io/ide | sh
then relaunch your nvim. You might got code completion, and goto definition features.
*Currently, it's only available for neovim

Categories