I'm trying to use Mustache together with i18n (php, within Wordpress). I've got the basic __ functionality working nicely, something like this
class my_i18n {
public function __trans($string) {
return __($string, 'theme-name');
}
}
class mytache {
public function __()
{
return array('my_i18n', '__trans');
}
}
Then to output a template with an i18n string, I can simply do this
$context = new mytache;
$template = "<div>{{#__}}String to translate{{/__}}</div>";
$m = new Mustache;
echo $m->render($template, $context);
So far everything is fine. However, I want to be able to translate strings with parameters. i.e. the equivalent of sprint_f(__('Account Balance: %s'), $balance);.
It seems that if I do something like {{#__}}Account Balance: {{balance}}{{/__}} it doesn't work. I'm guessing because the inner tag gets converted first and therefore the translation cannot be found for the phrase.
Any ideas how to achieve this cleanly with Mustache?
UPDATE: here's the end-result snippet (with massive help from bobthecow):
class I18nMapper {
public static function translate($str) {
$matches = array();
// searching for all {{tags}} in the string
if (preg_match_all('/{{\s*.*?\s*}}/',$str, &$matches)) {
// first we remove ALL tags and replace with %s and retrieve the translated version
$result = __(preg_replace('/{{\s*.*?\s*}}/','%s', $str), 'theme-name');
// then replace %s back to {{tag}} with the matches
return vsprintf($result, $matches[0]);
}
else
return __($str, 'theme-name');
}
}
class mytache {
public function __()
{
return array('I18nMapper', 'trans');
}
}
I added an i18n example here... it's pretty cheesy, but the test passes. It looks like that's almost the same as what you're doing. Is it possible that you're using an outdated version of Mustache? The spec used to specify different variable interpolation rules, which would make this use case not work as expected.
On my behalf I would suggest using normal, fully functional template engine. I understand, that small is great and everything, but for example Twig is much more advanced. So I would recommend it.
About mustache. Can't you just extend you translation method! For example you pass {{#__}}Account Balance: #balance#{{/__}}
function __( $string, $replacement )
{
$replaceWith = '';
if ( 'balance' == $replacement )
{
$replaceWith = 234.56;
}
return str_replace( '#' . $replacement . '#', $replaceWith, $string );
}
class my_i18n
{
public function __trans( $string )
{
$matches = array();
$replacement = '';
preg_match( '~(\#[a-zA-Z0-9]+\#)~', $string, $matches );
if ( ! empty( $matches ) )
{
$replacement = trim( $matches[0], '#' );
}
return __( $string, $replacement );
}
}
$Mustache = new Mustache();
$template = '{{#__}}Some lime #tag#{{/__}}';
$MyTache = new mytache();
echo $Mustache->render( $template, $MyTache );
This is a very ugly example, but you can make it fancy yourself. As I see Mustache on it's own will not be able to do what you want.
Hope that helped.
Related
I have a simple template engine that works fine the simple template, but I don't know how to adapt it to make it work with loops :
class Template {
public $template;
function getFile($file) {
$this->template = file_get_contents($file);
}
function set($tag, $content) {
$this->template = str_replace("{".$tag."}", $content, $this->template);
}
function ouput() {
eval("?>".$this->template."<?");
}
}
That's the loop I want to parse and display:
{{#each Stuff}}
{{Thing}} are {{Desc}}
{{/each}}
I dont want use any SMARTY or Twig engine.
Any idea please?
Ok, keep in mind this is just for learning purposes. You can't ask on SO for the whole code, you need to try and post question about your tries.
This code parse a string for a foreach and then executes it:
<?php
$var = array(2, 4);
$str = 'for i in var';
$a = explode(' ', $str);
foreach (${$a[3]} as $i => $value)
{
echo $value;
}
Read this part from PHP docs to understand what i did.
Previously I have been echoing $obj->html but a current project requires that the HTML be examined for slugs like {whatever} and replacing these with other content.
I have two problems. The first is that this code is slower than I would like:
class Foo {
function draw_content() {
$slug = "/(?<=\{).*(?=\})/";
if (preg_match($slug, $this->html, $matches)) {
foreach ($matches as $match) {
if (method_exists($this,$match)) {
$replacement = $this->$match();
$this->html = preg_replace("/\{$match\}/", $replacement, $this->html);
}
}
}
return $this->html;
} // fn
function new_releases() {
echo "new release book covers";
} // fn
} // class
Is there a better way to get the slug contents? I presume the regex is what is slowing this down?
The second issue is stranger to me. Given this $obj->html:
<p class="headline">New Releases</p>
<p>xxx{new_releases}xxxx</p>
The processed output of $obj->draw_content() is drawn by <?=$obj->draw_content()?>
new release book covers<p class="headline">New Releases</p>
<p>xxxxxxx</p>
Why is the new_releases() output prepended? The slug is gone but the replacement is not in it's place!
you can replace your pattern by:
$slug = '~{\K[^}]*+(?=})~';
IMHO, you should replace your preg_match test and your preg_replace by an only preg_replace_callback function, try something like this (and correct the bugs :).
function draw_content() {
$slug = '~{([^}]*+)}~';
$that = $this;
$this->html = preg_replace_callback( $slug, function ($m) use ($that) {
if (method_exists($that, $m[1]))
return $that->$m[1]();
return $m[0]; }, $this->html);
return $this->html;
}
I have one array for data
$data = array(title=>'some title', date=>1350498600, story=>'Some story');
I have a template
$template = "#title#, <br>#date(d)#<br> #date(m)#<br>#date(Y)#<br> #story#";
All i want is to fit data into template and i know that can be done by str_replace but my problem is the date format. date format is coming from the template not from the data, in data date is stored as php date.
yesterday i tried to ask the same question but i think my question wasn't clear.
Anybody please help me.
i think it won't work with str_replace easily so i'm going to use preg_replace
$data = array('title'=>'some title', 'date'=>1350498600, 'story'=>'Some story');
$template = "#title#, <br>#date(d)#<br> #date(m)#<br>#date(Y)#<br> #story#";
$result = preg_replace_callback('/#(\w+)(?:\\((.*?)\\))?#/', function ($match) use($data) {
$value = isset($data[$match[1]]) ? $data[$match[1]] : null;
if (!$value) {
// undefined variable in template throw exception or something ...
}
if (! empty($match[2]) && $match[1] == "date") {
$value = date($match[2], $value);
}
return $value;
}, $template);
Instead of using date(m) or date(Y) you could also do things like
date(d-m-Y) using this snippet
This has the disadvantage that you can format only the date variable using this mechanism. But with a few tweaks you can extend this functionality.
Note: If you use a PHP version below 5.3 you can't use closures but you can do the following:
function replace_callback_variables($match) {
global $data; // this is ugly
// same code as above:
$value = isset($data[$match[1]]) ? $data[$match[1]] : null;
if (!$value) {
// undefined variable in template throw exception or something ...
}
if (! empty($match[2]) && $match[1] == "date") {
$value = date($match[2], $value);
}
return $value;
}
$data = array('title'=>'some title', 'date'=>1350498600, 'story'=>'Some story');
$template = "#title#, <br>#date(d)#<br> #date(m)#<br>#date(Y)#<br> #story#";
// pass the function name as string to preg_replace_callback
$result = preg_replace_callback('/#(\w+)(?:\\((.*?)\\))?#/', 'replace_callback_variables', $template);
You can find more information about callbacks in PHP here
I'd suggest using a templating engine like so:
https://github.com/cybershade/CSCMS/blob/master/core/classes/class.template.php
And then your templates turn out like this:
https://github.com/cybershade/CSCMS/blob/master/themes/cybershade/site_header.tpl
and
https://github.com/cybershade/CSCMS/blob/master/modules/forum/views/viewIndex/default.tpl
Download this file: http://www.imleeds.com/template.class.txt
Rename the extension to .PHP from .TXT
This is something I created years ago, I keep my HTML away from my PHP, always. So see an example below.
<?php
include("template.class.php");
//Initialise the template class.
$tmpl = new template;
$name = "Richard";
$person = array("Name" => "Richard", "Domain" => "imleeds.com");
/*
On index.html, you can now use: %var.name|Default if not found% and also, extend further, %var.person.Name|Default%
*/
//Output the HTML.
echo $tmpl->run(file_get_contents("html/index.html"));
?>
I have the following variable:
$argument = 'blue widget';
Which I pass in the following function:
widgets($argument);
The widgets function has two variables in it:
$price = '5';
$demand ='low';
My questions is how can I do the following:
$argument = 'blue widget'.$price.' a bunch of other text';
widgets($argument);
//now have function output argument with the $price variable inserted where I wanted.
I don't want to pass $price to the function
price is made available once inside the function
Is there any sound way I can do this or do I need to rethink my design?
Off the top of my head, there are two ways to do this:
Pass in two arguments
widget($initText, $finalText) {
echo $initText . $price . $finalText;
}
Use a placeholder
$placeholder = "blue widget {price} a bunch of other text";
widget($placeholder);
function widget($placeholder) {
echo str_replace('{price}',$price,$placeholder);
}
// within the function, use str_replace
Here's an example: http://codepad.org/Tme2Blu8
Use some sort of placeholder, then replace it within your function:
widgets('blue widget ##price## a bunch of other text');
function widgets($argument) {
$price = '5';
$demand = 'low';
$argument = str_replace('##price##', $price, $argument);
}
See it here in action: http://viper-7.com/zlXXkN
Create a placeholder for your variables like this:
$argument = 'blue widget :price a bunch of other text';
in your widget() function, use a dictionary array and str_replace() to get your result string:
function widgets($argument) {
$dict = array(
':price' => '20',
':demand' => 'low',
);
$argument = str_replace(array_keys($dict), array_values($dict), $argument);
}
I would encourage preg_replace_callback. By using this method, we can easily use the captured values as a lookup to determine what their replacement should be. If we come across an invalid key, perhaps the cause of a typo, we can respond to this as well.
// This will be called for every match ( $m represents the match )
function replacer ( $m ) {
// Construct our array of replacements
$data = array( "price" => 5, "demand" => "low" );
// Return the proper value, or indicate key was invalid
return isset( $data[ $m[1] ] ) ? $data[ $m[1] ] : "{invalid key}" ;
}
// Our main widget function which takes a string with placeholders
function widget ( $arguments ) {
// Performs a lookup on anything between { and }
echo preg_replace_callback( "/{(.+?)}/", 'replacer', $arguments );
}
// The price is 5 and {invalid key} demand is low.
widget( "The price is {price} and {nothing} demand is {demand}." );
Demo: http://codepad.org/9HvmQA6T
Yes, you can. Use global inside your function.
$global_var = 'a';
foo($global_var);
function foo($var){
global $global_var;
$global_var = 'some modifications'.$var;
}
Consider changing the argument and then returning it from your widget function rather than simply changing it within the function. It will be more clear to people reading your code that $argument is being modified without having to read the function as well.
$argument = widget($argument);
function widget($argument) {
// get $price;
return $argument . $price;
}
I am trying to build a router function to properly match incoming URI's and match them to an array of stored system URI's. I also have wildcards '(:any)' and '(:num)' similar to CodeIgniter.
Basically, I am trying to get the 'admin/stats/(:num)' entry to match on both 'admin/stats' and admin/stats/1'.
While the script is starting I grab all paths from a separate array and use a foreach to save each path:
route('admin/stats/(:num)', array('#title' => 'Statistics',...));
The function is:
function route($path = NULL, $options = NULL) {
static $routes;
//If no arguments are supplied, return all routes stored.
if(!isset($path) && !isset($options)) {
return $routes;
}
//return options for path if $path is set.
if(isset($path) && !isset($options)) {
//If we have an exact match, return it.
if(array_key_exists($path, $routes)) {
return $routes[$path];
}
//Else, we need to use RegEx to find the correct route options.
else {
$regex = str_replace('/', '\/', $path);
$regex = '#^' . $regex . '\/?$#';
//I am trying to get the array key for $route[$path], but it isn't working.
// route_replace('admin/stats/(:num)') = 'admin/stats/([0-9]+)'.
$uri_path = route_replace(key($routes[$path])); //route_replace replaces wildcards for regex.
if(preg_match($regex, $uri_path)) {
return $routes[$path];
}
}
}
$routes[$path] = $options;
return $routes;
}
Route replace function:
function route_replace($path) {
return str_replace(':any', '.+', str_replace(':num', '[0-9]+', $path));
}
A key/value pair in the $routes array looks like:
[admin/stats/(:num)] => Array
(
[#title] => Statistics //Page title
[#access] => user_access //function to check if user is authorized
[#content] => html_stats //function that returns HTML for the page
[#form_submit] => form_stats //Function to handle POST submits.
)
Thanks for the help. This is my first router and I am not that familiar in making proper Regex's.
'admin/stats/(:num)' will never match 'admin/stats' as in your "pattern" the slash is required. In pseduo-regex you need to do something like 'admin/stats(/:num)'.
There does also seem to be a few bugs in your code. This line
$uri_path = route_replace(key($routes[$path]));
is in the block that is executed when $path is not a key that exists in $routes.
I've tried to rewrite it and this seems to work (this is just the else clause):
foreach( array_keys( $routes ) as $route ) {
$regex = '#^' . $route . '?$#';
//I am trying to get the array key for $route'$path', but it isn't working.
// route_replace('admin/stats/(:num)') = 'admin/stats/('0-9'+)'.
$uri_path = route_replace($regex); //route_replace replaces wildcards for regex.
if(preg_match($uri_path,$path)) {
return $routes[$route];
}
}
But this requires 'admin/stats/(:num)' to be 'admin/stats(/:num)'.
btw if you don't have one already, you should get a debugger (Zend and xDebug are two of the most common ones for PHP). They can be invaluable in solving problems like this.
Also, ask yourself if you need to write a router, or whether you can't just use one of the perfectly good ones out there already...