I am writing some code, but I got stuck at the point where I wanted to implement a language system apart from the database (maybe integrating it later).
At the moment I have these lines of code in my language class:
final public function findTexts()
{
global $engine;
$file = file_get_contents('./styles/'.$engine->getStyle().''.$engine->getPage());
if(strpos($file, '%%'))
{
echo "String found";
}
}
Let me show you my other function:
final public function getText($text)
{
global $engine, $LANG;
$text = str_ireplace($text, $LANG[substr($text, 2, -2)], $text);
echo $text;
}
What I wanted to do, is check a file (which is inside a styles folder) for a string (for example %%Hi%%). Inside my language file I says Hi == Hello, so when I do the getText function it will say 'Hello'. But, I want to make it simple so you only have to type %%Hi%% inside your file, and the language class will automatically check all strings containing %% at the beginning, and %% at the end, and will replace the string inside the %% and %% with the given string inside the language file.
Hope that is enough info...
Many thanks!
You can accomplish this by using regular expressions and preg_replace_callback. See my example below:
<?php
final public function findTexts()
{
global $engine;
$file = file_get_contents('./styles/'.$engine->getStyle().''.$engine->getPage());
$language = $this;
$file = preg_replace_callback('/(%%([^%]+)%%)/',function($matches) use ($language){
$word = $matches[2];
return $language->getText($word);
},$file);
}
This method can become very slow if you have a lot of files or a lot of replacements. It's great if you are doing a very small personal website. If you're doing something a bit bigger, I highly recommend caching the result so you don't have to recompute it for every request and file.
Related
This has probably been asked before, but I couldn't find the answer to my question with some preliminary searches, so here it is:
A very simple example of my current method of injecting PHP variables into HTML is as follows:
HTML (file.php):
<tag><?php echo $variable; ?></tag>
PHP:
$variable = "value";
ob_start();
include "file.php";
$results = ob_get_clean();
This achieves the correct result just fine, but it annoys me every time that I have to copy and paste those three lines to get variables injected into my HTML files. There's a chance I might want to change the details of this injection functionality at a later date, and it's currently scattered in a couple hundred locations throughout my code.
In a perfect world, I would move those three lines to a separate function that takes the file path as an input, and returns the "compiled" result. However, the function would then no longer have access to the calling scope, and I would have to pass the variables in as another input. The only thing I'm able to think of for doing that is:
function injectVariables($filePath, array $variables)
{
//Built-in PHP function, extracts array key => value pairs into variable $key = value pairs in the current scope
extract($variables);
ob_start();
include $filePath;
return ob_get_clean();
}
That certainly gets the job done, and it will be what I implement if there aren't any better solutions, but it seems less than ideal to me. It involves creating an array every time this runs, just to loop through it and extract all the variables, which just feels a bit wrong to me.
Does anyone know of any other methods that might work?
Not really sure what you are asking, but here is my answer
I don't know the structure of your code, but I hope you are using a MVC approach (or at least something which deals with classes) so that you can do the following:
In your main controller, you create a class variable like viewData which will be an array. And you put in it everything you want
$this->viewData['username'] = $username;
$this->viewData['myArray'] = array('foo' => 'bar');
$this->viewData['menuSubview'] = 'path_to_menu_subview.php';
Then, you create a render function.
public function render()
{
extract($this->viewData);
ob_start();
include("myfile.php");
return ob_get_clean();
}
And in myfile.php (with the HTML) you can simply do what you used so far
<div id="menu"><?php include($menuSubview);?></div>
<p><?=$username;?></p>
<p><?=$myArray['foo'];?></p>
The whole code can be something like this.
class Something {
protected $viewData;
protected $viewFile;
public function logic()
{
$this->userProfile();
echo $this->render();
}
public function userProfile()
{
$this->viewData['username'] = 'John The Ripper';
$this->viewFile = 'myFile.php';
}
public function render()
{
extract($this->viewData);
ob_start();
include($this->viewFile);
return ob_get_clean();
}
}
Here's a simplified class that stores data and allows for recursive rendering of files that all have access to the save data form the initial instance.
class View {
// view data
protected $_data = [];
/**
* Set view data
*/
public function __set( $key, $value )
{
$this->_data[ $key ] = $value;
}
/**
* Get view data
*/
public function __get( $key )
{
if( array_key_exists( $key, $this->_data ) )
{
return $this->_data[ $key ];
}
return null;
}
/**
* Render view file
*/
public function render( $file )
{
if( is_file( $file ) )
{
$view = $this;
ob_start();
include( $file );
return ob_get_clean();
}
return 'File not found: '.$file;
}
}
Just use the variable $view inside your included files to access the data in the class, or render() another partial file that can do the same thing, and so on.
// Bootstrap a View instance and add some data
$view = new View;
$view->dataOne = 'valueOne';
$view->dataTwo = 'valueTwo';
// Render main template
echo $view->render( 'template.php' );
Inside template.php
<header>
<?= $view->render( 'header.php' ) ?>
</header>
<h1><?= $view->dataOne ?></h1>
<p><?= $view->dataTwo ?></p>
Nothing is wrong with your injectVariables() function, it is in fact a good idea.
You shouldn't have performance impact anyway (If this is your main concern your are doing premature optimization !)
Your view (HTML) shouldn't know about the internal of your application. (You split responsability - this is a huge subject that I won't talk deep)
You know if something end-up into $variables it has been build/formatted or is revelent to be display for $filePath.
In complex system you may end up with a variable with a pdf generator, why would the HTML want that? The only purpose of the HTML is to DISPLAY HTML data.
Some variables from your logic will end-up in $variables almost every time such as session informations (Who is currently logged), for your case this is normal.
In the perfect world if you put a class into $variables it should be designed only for purpose of your HTML even if it is almost the same object as your logic.
As an exemple I designed a Session class in my logic. It does have method such as Logout() and EnsureIsAlive() as well as field such as FullName. FullName is going to be use by my HTML because I want to display it in the header of my page.
I should have a Session class for the HTML that only contains a FullName field because the HTML has only one job and is it to display data. Login() and EnsureIsAlive() are specific for the logic and aren't related at all with displaying data.
In reallity time is always a constraint and because you are writing all by yurself from scratch you may end-up just sending the logic Session class into $variables.
Design note : I'm a C# programmer and we always use class over array to pass parameters as a good pratice and it does affect the exemple about how to pass FullName to your HTML. In your case, instead of creating a class dedicated for the HTML you could also create a simple array :
$variables['Session'] = array('FullName' => $mySession->fullName).
That way you still avoid your HTML to have access to the unrelated method specific for your logic. I'm not sure if it is a good pratice in php...
Keep context variables clean
If you do call injectVariables() more than once (for different $PathFile) you may not want the same variables.
One injectionVariables() could be for a widget, another for a complete complete page.
Why would a TimeWidget need your totalInvoice variable?
You also have the ability to prepare multiples $variables at the same time because each injectVariables() will have his own parameters.
I'm not up-to-date but I know phpBB template system use (back near 2000) the exact same pattern as your injectVariables()).
You wrote that each of your page do call injectVariables(), you could also (but it may be complex) do it only in one file - like php frameworks.
Framework handle HTTP requests into one specific file
(eg. http://yoursite.com/hello/world or http://yoursite.com/login would call (internaly) http://yoursite.com/index.php?page=hello&subpage=world and http://yoursite.com?page=login).
Then this page (index.php in my exemple) would include a specific file (controller) according with his GET parameters ($_GET['page'] and $_GET['subpage']).
The controller job would be to fetch data, apply logic then fill $variables.
Then index.php will call injectVariables() with the releated HTML.
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 ;)
So I want to make my page to have multi language function, like German, English, and Hungarian. I started to learn OOP in PHP, and now I force myself to use OOP in everything, sure not a good idea, but I try to learn. So this is what I got so far:
<?php
class class_lang {
private $language;
public function __construct() {
$this->language = $_GET['lang'];
}
public function select_lang($var);
return ('language_'.$this->select_lang($var));
}
?>
So what is going on. On my index page i got 3 links (national flags) when i click 1 my class gets the value from the url like this: href="index.php?lang=uk". Now what i am not sure about is how do i make the string: cause my lang files i want to include look like language_uk.php , language_ge.php etc... So i just want to creat that string and pass it back to my index page in a value so i can include then the right file.
If I understand it correctly, this should work:
return 'language_' . $this->language;
try to make public var $language ,
construct could filter and validate the GET var, save the object somewhere (sessions) to interact with object
additional methods you could use are
ChangeLang
SetLang
LoadLangFile
SaveToSession
many other..
(1) In PHP, field members are declared with leading "$", but, later, witouth it, and prefixed with "$this->". So:
$language;
Becomes:
$this->language
(2) Extra, remove the semicolon from the method header:
public function select_lang($var)
{
return ('language_' . $this->language($var));
}
(3) Extra, add a temporally variable to methods that return a value, its not required by syntax, but, will allow you to debug ("ToString"), and avoid a lot of problems:
public function select_lang($var)
{
$Result = ('language_' . $this->language)
return $Result;
}
Cheers.
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.