70 if(!$this->_prefix) $this->_prefix = \Rsi\Str::random();
71 foreach([
'{',
'[',
']',
'}'] as $char) $this->_unescape[$this->_escape[
'\\' . $char] = $this->_prefix . bin2hex($char)] = $char;
72 $this->
publish([
'langId'],self::READWRITE);
79 return [$this->langId => null];
82 protected function error($message,$file = null,$line_no = null,$context = null){
83 $this->
component(
'log')->add($this->logPrio,$message,$file,$line_no,$context);
92 return array_change_key_case(require(str_replace(
'*',$lang_id,$this->_filename)));
100 public function getStr($id,$default =
false){
101 $id = explode(
'.',$id);
102 $lang_id = count($id) > 1 ? $id[1] : $this->langId;
103 if(!array_key_exists($lang_id,$this->_strs)) $this->_strs[$lang_id] = array_change_key_case($this->
getStrs($lang_id));
104 return array_key_exists($id = strtolower($id[0]),$this->_strs[$lang_id]) ? $this->_strs[$lang_id][$id] : $default;
107 protected function macro($str,$type,&$tags,$callback){
109 strpos($str,
'<' . ($tag = $this->
namespace .
':' . $type)) &&
110 preg_match_all(
"/<$tag(?<attribs>(" . \
Rsi\Str::ATTRIBUTES_PATTERN .
")*)>(?<str>.*?)<\\/$tag>/s",$str,$matches,PREG_SET_ORDER)
111 )
foreach($matches as $match)
try{
112 $str = str_replace($match[0],call_user_func_array($callback,[$match[
'str'],\
Rsi\Str::attributes($match[
'attribs']),&$tags]),$str);
115 return $this->
error($e,[
'macro' => $type]);
121 $str = strtr($this->
macro($str,self::MACRO_ESCAPE,$tags,
function($str){
122 return strtr($str,array_flip($this->_unescape));
124 $str = $this->
macro($str,self::MACRO_CACHE,$tags,
function($str,$attribs,&$tags){
125 if(!($key = $attribs[
'key'] ?? null)) $key = md5($str);
126 elseif(preg_match_all(
'/\\[([\\w\\-]+)\\]/',$key,$matches,PREG_SET_ORDER))
foreach($matches as list($full,$tag)){
127 if(is_array($value = $this->
value($tag,null,$tags))) $value = md5(serialize($value));
128 $key = str_replace($full,$value,$key);
130 $key .=
'-' . $this->langId;
131 $ttl = $attribs[
'ttl'] ?? 0;
132 if(!array_key_exists($key,$this->_cache)) $this->_cache[$key] = $ttl ? $this->
component(
'cache')->fetch($this->cacheKeyPrefix . $key) :
false;
133 $exists = $this->_cache[$key] !==
false;
134 $tag = $this->
namespace . ':' . self::MACRO_CACHE;
135 return "<$tag key='$key'" . ($exists ? '' : " ttl='$ttl'") . '>' . ($exists ? null : $str) . "</$tag>";
137 $str = $this->
macro($str,self::MACRO_CONST,$tags,function($name){
138 return constant($name);
140 $str = $this->
macro($str,self::MACRO_UNITS,$tags,function($tag,$attribs,&$tags){
142 foreach(\Rsi\Number::units($value = $tags[$tag] ?? 0,\Rsi\Record::explode($attribs['units'] ?? null,',','='),$value * ($attribs['tolerance'] ?? 0) / 100) as $unit => $count)
143 if($count || array_key_exists('empty',$attribs)) $result[] = compact('unit','count');
144 $tags[$attribs['tag'] ?? $tag . 'Units'] = $result;
147 $str = $this->
macro($str,self::MACRO_LOCATION,$tags,function($str){
148 return htmlspecialchars($this->component('location')->rewrite($str));
150 $str = $this->
macro($str,self::MACRO_URL,$tags,function($url,$attribs){
151 return $this->
component('cache')->fetch($this->urlKeyPrefix . $url,$attribs['ttl'] ?? $this->urlTtl,function() use ($url){
152 $this->
component(
'log')->debug(
"Loading external content from '$url'",__FILE__,__LINE__);
153 return preg_match(
'/^https?:\\/\\/(?!localhost|127\\.0\\.0\\.|\\[[0:]+1\\])/i',$url) ? file_get_contents($url) : $url;
156 $str = $this->
macro($str,self::MACRO_RIGHT,$tags,
function($str,$attribs){
157 return ($this->
component(
'user')->authorized($attribs[
'right'] ?? null,$attribs[
'level'] ?? null) xor ($attribs[
'not'] ??
false)) ? $str : null;
164 $str = $this->
macro($str,self::MACRO_CACHE,$tags,
function($str,$attribs){
165 $key = $attribs[
'key'];
166 if(($ttl = $attribs[
'ttl'] ??
false) ===
false)
return $this->_cache[$key];
167 if($ttl) $this->
component(
'cache')->save($this->cacheKeyPrefix . $key,$str,$ttl);
168 return $this->_cache[$key] = $str;
170 $str = $this->
macro($str,self::MACRO_FRAGMENT,$tags,
function($str,$attribs){
171 $fragments = $this->session->fragments ?: [];
172 $fragments[$id = $attribs[
'id'] ??
'fragment-' . \Rsi\Str::random()] = $str;
173 $this->session->fragments = $fragments;
174 return ($delay = $attribs[
'delay'] ?? null)
175 ?
"<div class='fred-fragment' id='$id'><script>document.addEventListener('DOMContentLoaded',function(){" .
176 "setTimeout(function(){ fred.controller.fragment('#$id','$id') },$delay * 1000)" .
177 "},true)</script></div>" 180 $str = $this->
macro($str,self::MACRO_ROUTE,$tags,
function($controller_name,$params){
181 $controller_name = explode(
'.',$controller_name);
182 if(array_key_exists(
'_',$params)){
183 foreach(($params[
'_'] ==
'*' ? array_keys($_GET) : explode(
',',$params[
'_'])) as $key)
184 if(!array_key_exists($key,$params) && ($value = $this->
component(
'request')->complex($key))) $params[$key] = $value;
187 return htmlspecialchars($this->
component(
'router')->reverse($controller_name[0],$controller_name[1] ?? null,$params));
189 $str = $this->
macro($str,self::MACRO_SPACE,$tags,
function($str){
190 return trim(preg_replace(
'/\\s*\\n\\s*/',
"\n",preg_replace(
'/>\\s+</',
'><',$str)));
192 $str = $this->
macro($str,self::MACRO_REPLACE,$tags,
function($str,$attribs){
193 if(!array_key_exists(
'from',$attribs))
return $str;
194 if(substr($from = $attribs[
'from'],0,1) !=
'/') $from =
'/' . preg_quote($from,
'/') .
'/';
195 return preg_replace($from,$attribs[
'to'] ?? null,$str);
197 $str = $this->
macro($str,self::MACRO_IMAGE,$tags,
function($url,$attribs){
198 return $this->
component(
'cache')->fetch($this->imageKeyPrefix . $url .
'?' . http_build_query($attribs),$attribs[
'ttl'] ?? $this->imageTtl,
function() use ($url,$attribs){
199 unset($attribs[
'ttl']);
200 foreach([
'width',
'height',
'background'] as $key)
if(array_key_exists($key,$attribs) && ($attribs[$key] ==
'auto')) $attribs[$key] =
true;
201 return $this->
component(
'html')->img($url,$attribs);
214 if($tags)
foreach($tags as $tag => $value){
215 switch(substr($tag,0,1)){
216 case '!': $tag = substr($tag,1);
218 default:
if(!is_object($value)) $value = is_array($value) ? $this->
escape($value) : htmlspecialchars($value);
220 $escaped[$tag] = $value;
225 protected function value($tag,$params,&$tags){
226 if(\
Rsi\Str::numeric($tag))
return $tag;
229 foreach(explode(
'.',$tag) as $key){
230 if(is_object(($value)))
try{
231 $value = $this->
escape([$value->$key])[0];
234 $this->
error(
"Property '$key' does not exists for tag '$tag'",__FILE__,__LINE__,compact(
'value'));
238 elseif(!array_key_exists($key,$value)){
242 else $value = $value[$key];
245 return is_object($value) ? strval($value) : $value;
248 $this->
error(
"Could not convert object for tag '$tag' to string",__FILE__,__LINE__,compact(
'value'));
250 foreach($tags as $key => $value)
if((substr($key,0,1) ==
'@') && !strcasecmp($tag,trim($key,
'@!'))){
251 $value = [$tag => call_user_func($value,$tag,$tags,$params)];
252 $tags += substr($key,1,1) ==
'!' ? $value : $this->
escape($value);
255 return $tags[$tag] = null;
259 $expr = strtr($expr,[$this->_prefix .
true =>
'',
'?' => is_array($tag) ? implode(
',',$tag) : $tag]);
260 foreach($tags as $key => $value)
if(preg_match($mask =
'/\\b' . preg_quote($tag = trim($key,
'@!'),
'/') .
'\\b/',$expr)){
261 if(is_array($value = $this->
value($tag,null,$tags))) $value = implode(
',',$value);
262 elseif(preg_match(
'/^\\d{4}-\\d{2}-\\d{2}/',$value)) $value =
"'$value'";
263 $expr = preg_replace($mask,$value,$expr);
265 return \Rsi\Number::evaluate($expr);
269 return $length === null ? substr($value,$start) : substr($value,$start,$length);
273 return str_replace($search,$replace,$value);
277 return preg_replace($pattern,$replace,$value);
287 protected function format($tag,$value,$params,&$tags){
288 foreach(explode(
'|',$params) as $format)
if($format)
try{
289 if(substr($format,0,1) ==
'=') $value = $this->
evaluate(substr($format,1),$tag,$tags);
290 elseif(preg_match(
'/(\w+)\\((.*)\\)$/',$format,$match)){
291 $func_name =
'format' . ucfirst($match[1]);
292 $params = array_merge([$value],$match[2] ? array_map(
'urldecode',explode(
',',$match[2])) : []);
293 if(method_exists($component = $this,$func_name) || method_exists($component = $this->
component(
'local'),$func_name))
294 $value = call_user_func_array([$component,$func_name],$params);
295 else $value = call_user_func_array(
297 array_merge([self::EVENT_FORMAT . $match[1],$this],$params)
301 $value = \Rsi\Str::transform($value,$format,$count);
302 if(!$count) $value = date($format,strtotime($value));
306 $value = $this->
error($e,compact(
'tag',
'value',
'params'));
309 return is_array($value) ? false : $value;
324 protected function replace($str,$full,$type,$tag,$operator,$params,$value,&$tags,&$strs){
325 $default = $replace = $raw =
false;
326 if($type)
switch($type){
328 $tags[$tag] = substr($params,1);
331 case self::TYPE_OPTIONAL:
333 case self::TYPE_SUBSTR:
334 if(!in_array($tag,$strs)) $strs[] = $tag;
335 $replace = \Rsi\Str::transform($this->
preProcess($this->
getStr($tag,$default),$tags),$params = explode(
'|',$params));
336 $raw = !in_array(self::MACRO_ESCAPE,$params);
338 case self::TYPE_TRUE:
339 if(substr($params,0,2) ==
'|=') $value = $this->
evaluate(substr($params,2),$tag,$tags);
340 if($value) $replace = $this->_prefix .
true;
343 case self::TYPE_FALSE:
344 if(substr($params,0,2) ==
'|=') $value = $this->
evaluate(substr($params,2),$tag,$tags);
345 if(!$value) $replace = $this->_prefix .
true;
348 case self::TYPE_COUNT:
349 $replace = count($value);
353 if(is_array($value)){
354 $params = explode(
'|',substr($params,1),2);
355 $replace = $this->
format($tag,array_sum(\
Rsi\Record::column($value,$params[0])),$params[1] ?? null,$tags) . $this->_prefix .
true;
358 case self::TYPE_ARRAY:
359 if($value && (is_array($value) || (is_numeric($value) && ($value > 0) && ($value = range(1,$value))))){
360 $replace = $this->_prefix .
true;
361 $sub = strtr(substr($params,1),$this->_unescape);
364 foreach($value as $key => $record) $replace .=
365 $this->
trans($sub,(is_array($record) ? $record : []) + [
'v' => $record,
'k' => $key,
'i' => $i++,
'r' => --$r] + $tags);
368 case self::TYPE_DECODE:
369 $options = explode(
'|',substr($params,1));
370 $random = $value ==
'?' ? rand(0,count($options) - 1) :
false;
371 foreach($options as $index => $option){
372 $i = strpos($option,
':');
373 if($i ===
false) $replace = $option;
374 elseif($random ===
false ? substr($option,0,$i) == $value : $index == $random){
375 $replace = substr($option,$i + 1);
378 if(!\Rsi::nothing($replace)) $replace .= $this->_prefix .
true;
380 $replace = $this->
trans(strtr($replace,$this->_unescape),$tags);
382 case self::TYPE_JSON:
383 $replace = json_encode($value);
386 $replace = $this->
format($tag,$value,$params,$tags);
390 if(\
Rsi\Str::operator(strtr(is_array($value) ? count($value) : $value,array_flip($this->_unescape)),$operator,$params)) $replace = $this->_prefix .
true;
393 elseif(!\Rsi::nothing($value))
394 $replace = $this->
format($tag,$value,$params,$tags) . $this->_prefix .
true;
395 return str_replace($full,$raw ? $replace : strtr($replace,array_flip($this->_unescape)),$str);
403 while($str && preg_match_all(
'/{([^{}]*?)}/',$str,$matches,PREG_SET_ORDER))
foreach($matches as list($full,$inner))
404 $str = str_replace($full,strpos($full,$this->_prefix .
true) ? str_replace($this->_prefix .
true,
'',$inner) :
'',$str);
415 protected function trans($str,$tags = null){
416 if(!$str)
return $str;
417 $tags = array_merge($this->_tags,$tags ?: []);
419 if(($str = $this->
preProcess($str,$tags)) && $tags)
foreach($tags as $key => $value)
420 if(is_scalar($value)) $str = str_replace(
"[$key]",\Rsi::nothing($value) ?
false : strtr($value,array_flip($this->_unescape)) . $this->_prefix .
true,$str);
421 while($str && preg_match_all($this->regex,$str,$matches,PREG_SET_ORDER)){
422 foreach($strs as $id){
423 if(!array_key_exists($id,$count)) $count[$id] = 0;
424 elseif(++$count[$id] >= $this->maxNested){
425 $this->
error(
"Sub-string '$id' nested more than {$this->maxNested} times",__FILE__,__LINE__,compact(
'str',
'tags'));
430 foreach($matches as $match){
431 list($full,$type,$tag,$operator,$params) = $match;
432 if(!in_array($full,$done)){
433 if(substr($type,0,1) ==
'{') $type = $this->types[substr($type,1,-1)] ?? null;
434 $value = in_array($type,[self::TYPE_TAG,self::TYPE_OPTIONAL,self::TYPE_SUBSTR]) ? null : $this->
value($tag,$params,$tags);
435 $str = $this->
replace($str,$full,$type,$tag,$operator,$params,$value,$tags,$strs);
440 return $this->
postProcess(str_replace($this->_prefix .
true,
'',strtr($this->
brush($str),$this->_unescape)),$tags);
448 public function str($str,$tags = null){
458 public function id($id,$tags = null,$default =
false){
459 return $this->
str($this->
getStr($id,$default),$tags);
463 $fragments = $this->session->fragments ?: [];
464 $fragment = $fragments[$id] ??
false;
465 unset($fragments[$id]);
466 $this->session->fragments = $fragments;
472 $this->_regex =
'/\\[(';
473 foreach($this->types as $type) $this->_regex .= preg_quote($type,
'/') .
'|';
476 '(\\w[\\w\\-\\/\\.]*\\w|\\w+)' .
477 '(==|!=|>=?|<=?|%|=%|-?\\*-?|\\/\\/)?' .
485 $this->_tags = $value ===
false ? [] : array_merge($this->_tags,$value);
491 foreach($this->
constants(
'TYPE_') as $name => $value) $this->_types[strtolower($name)] = $value;
497 return $this->
id($key);
500 public function __call($func_name,$params){
501 return $this->
id($func_name,array_shift($params),array_shift($params));
505 return $this->
str($str,$tags);
const TYPE_FALSE
Show section when tag value is false: {[!tag]show when tag is empty}.
const TYPE_SUBSTR
Points to a sub-string: [@strId].
format($tag, $value, $params, &$tags)
Format a value.
const TYPE_JSON
Insert a JSON encoded value.
const MACRO_SPACE
Remove white-space between tags.
replace($str, $full, $type, $tag, $operator,$params, $value, &$tags, &$strs)
Replace a tag with its value.
$_tags
Default tags to use with every translation.
const MACRO_FRAGMENT
Replace the tag with a placeholder and replace it with the fragment later on (with.
const INFO
Informational message.
const TYPE_TRUE
Show section when tag value is true: {[tag]show when tag is not empty}.
$_filename
Filename for the translations (asterisk will be replace by the langId).
__call($func_name, $params)
formatRegex($value, $pattern, $replace)
formatReplace($value, $search, $replace)
const TYPE_SUM
Sum of an array column: [=array|column|formatting].
__invoke($str, $tags=null)
postProcess($str, &$tags)
const MACRO_LOCATION
Rewrite the location between the tags (pre processing).
trans($str, $tags=null)
Translate a string.
formatSubstr($value, $start, $length=null)
error($message, $file=null, $line_no=null, $context=null)
const TYPE_OPTIONAL
Optional sub-string (defaults to empty; no error): [?strId].
langs()
Available languages.
const TYPE_DECODE
Decode a value: [:intTag|1:foo|2:bar|default]. Set the value to '?' for a random selection.
$logPrio
Prio for translation related errors (invalid tags, format, etc).
constants($prefix=null)
Return all constants.
const MACRO_CONST
Replace the text with the value of the constant between the tags (pre processing).
const MACRO_IMAGE
Generate an image tag for the image between the tags (the content is cached, set the 'ttl'...
const TYPE_ARRAY
Repeat section for each item (item aded to tags, including k = key, v = value - when record is...
const MACRO_ROUTE
Create a (reverse) route for the controller name between the tags (attributes are params)...
id($id, $tags=null, $default=false)
Translate a string by ID.
$maxNested
Maximum number of times a sub-string may be nested.
str($str, $tags=null)
Translate a string.
const MACRO_REPLACE
Replace the text in attribute 'from' (regex) to 'to'.
publish($property, $visibility=self::READABLE)
Publish a property (or hide it again).
value($tag, $params, &$tags)
evaluate($expr, $tag, &$tags)
escape($tags)
Escape tag values (convert special characters to HTML entities).
const MACRO_URL
Load external content from the address between the tags (the content is cached, set the 'ttl'...
$imageTtl
Default cache TTL for image tag generation.
brush($str)
Elaborate conditional parts.
getStrs($lang_id)
Get all strings.
const TYPE_COUNT
Insert number of items in array: Array has [#arrayTag] items.
macro($str, $type, &$tags, $callback)
$urlTtl
Default cache TTL for external content.
const MACRO_ESCAPE
Escape the text between these tags (pre processing).
getStr($id, $default=false)
Basic string getter.
const TYPE_TAG
Add a tag to to the tags: [+tag|value].
const MACRO_UNITS
Split the value of the tag name into units, so it can be used as an array (units in desired...
const TYPE_RAW
Insert value without inserting a conditional tag: [[~intTag],number].
component($name)
Get a component (local or default).
const MACRO_CACHE
Cache the (processed) text (set the 'ttl' tag to use the main cache; use the 'key'.