FRED™  3.0
FRED™: Framework for Rapid and Easy Development
Trans.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Rsi\Fred;
4 
5 /**
6  * Translations component.
7  */
8 class Trans extends Component{
9 
10  const TYPE_TAG = '+'; //!< Add a tag to to the tags: [+tag|value].
11  const TYPE_OPTIONAL = '?'; //!< Optional sub-string (defaults to empty; no error): [?strId].
12  const TYPE_SUBSTR = '@'; //!< Points to a sub-string: [\@strId].
13  const TYPE_TRUE = '%'; //!< Show section when tag value is true: {[%tag]show when tag is not empty}.
14  const TYPE_FALSE = '!'; //!< Show section when tag value is false: {[!tag]show when tag is empty}.
15  const TYPE_COUNT = '#'; //!< Insert number of items in array: Array has [\#arrayTag] items.
16  const TYPE_SUM = '='; //!< Sum of an array column: [=array|column|formatting].
17  const TYPE_ARRAY = '*'; //!< Repeat section for each item (item aded to tags, including k = key, v = value - when record is
18  // not an array, i = index, r = reverse index): Values: [*arrayTag|{\\[i\\], }\\[v\\]].
19  const TYPE_DECODE = ':'; //!< Decode a value: [:intTag|1:foo|2:bar|default]. Set the value to '?' for a random selection.
20  const TYPE_JSON = '$'; //!< Insert a JSON encoded value.
21  const TYPE_HTML = '&'; //!< Insert an HTML block: [&contentTag|htmlTag|params].
22  const TYPE_RAW = '~'; //!< Insert value without inserting a conditional tag: [[~intTag],number].
23 
24  const MACRO_ESCAPE = 'escape'; //!< Escape the text between these tags (pre processing).
25  const MACRO_SECTION = 'section'; //!< Define a section which content can be redefined later on (use the 'id' attribute). The
26  // content is processed afterwards.
27  const MACRO_FRAGMENT = 'fragment'; //!< Replace the tag with a placeholder and replace it with the fragment later on (with
28  // 'delay' attribute) or on demand (set the 'id' attribute).
29  const MACRO_SPACE = 'space'; //!< Remove white-space between tags.
30  const MACRO_REPLACE = 'replace'; //!< Replace the text in attribute 'from' (regex) to 'to'.
31  const MACRO_UNITS = 'units'; //!< Split the value of the tag name into units, so it can be used as an array (units in desired
32  // order in the 'units' tag - e.g. 'km=1000,m=1,cm=0.01'; use the 'tolerance' tag to set a cut-off percentage; set the 'tag'
33  // attribute to choose a custom result tag - defaults to current tag + 'Units'; add an 'empty' attribute to include units
34  // without values, if there is a higher unit with a value; pre processing).
35  const MACRO_CONST = 'const'; //!< Replace the text with the value of the constant between the tags (pre processing).
36  const MACRO_CACHE = 'cache'; //!< Cache the (processed) text (set the 'ttl' tag to use the main cache; use the 'key'
37  // attribute to define a custom cache key, otherwise one is generated; the key can contain tags).
38  const MACRO_ROUTE = 'route'; //!< Create a (reverse) route for the controller name between the tags (attributes are params).
39  const MACRO_LOCATION = 'location'; //!< Rewrite the location between the tags (pre processing).
40  const MACRO_IMAGE = 'image'; //!< Generate an image tag for the image between the tags (the content is cached, set the 'ttl'
41  // attribute to deviate from the default $imageTtl).
42  const MACRO_URL = 'url'; //!< Load external content from the address between the tags (the content is cached, set the 'ttl'
43  // attribute to deviate from the default $urlTtl; pre processing)).
44  const MACRO_RIGHT = 'right'; //<! Only include the text when the user is authorized for the 'right' attribute (set an
45  // optional 'level'; add a 'not' attribute for the other way round; pre processing).
46 
47  const EVENT_FORMAT = 'trans:format:';
48 
49  public $namespace = 'trans';
50  public $maxNested = 10; //!< Maximum number of times a sub-string may be nested.
51  public $logPrio = Log::INFO; //!< Prio for translation related errors (invalid tags, format, etc).
52  public $minLogSteps = 5; //!< Only log translation steps with more than this number of steps.
53  public $imageTtl = 86400; //!< Default cache TTL for image tag generation.
54  public $imageKeyPrefix = 'trans-image-';
55  public $urlTtl = 86400; //!< Default cache TTL for external content.
56  public $urlKeyPrefix = 'trans-url-';
57  public $cacheKeyPrefix = 'trans-cache-';
58 
59  protected $_langId = null;
60  protected $_filename = null; //!< Filename for the translations (asterisk will be replace by the langId).
61  protected $_prefix = null;
62  protected $_strs = [];
63  protected $_tags = []; //!< Default tags to use with every translation.
64 
65  protected $_section = [];
66  protected $_sectionIndex = 0;
67  protected $_escape = [];
68  protected $_unescape = [];
69  protected $_cache = [];
70 
71  protected $_types = null;
72  protected $_regex = null;
73 
74  protected function init(){
75  parent::init();
76  if(!$this->_prefix) $this->_prefix = \Rsi\Str::random();
77  foreach(['{','[',']','}'] as $char) $this->_unescape[$this->_escape['\\' . $char] = $this->_prefix . bin2hex($char)] = $char;
78  $this->publish(['langId'],self::READWRITE);
79  }
80  /**
81  * Available languages.
82  * @return array Key = ID, value = description.
83  */
84  public function langs(){
85  return [$this->langId => null];
86  }
87 
88  protected function error($message,$file = null,$line_no = null,$context = null){
89  $this->component('log')->add($this->logPrio,$message,$file,$line_no,$context);
90  return 'error';
91  }
92  /**
93  * Get all strings.
94  * @param string $lang_id Language ID.
95  * @return array Key = string ID, value = string.
96  */
97  protected function getStrs($lang_id){
98  return require(str_replace('*',$lang_id,$this->_filename));
99  }
100  /**
101  * Basic string getter.
102  * @param string $id String ID.
103  * @param string $default Default string when ID is not found.
104  * @return string
105  */
106  public function getStr($id,$default = false){
107  $id = explode('.',$id);
108  $lang_id = $id[1] ?? $this->langId;
109  if(!array_key_exists($lang_id,$this->_strs)) $this->_strs[$lang_id] = array_change_key_case($this->getStrs($lang_id));
110  return array_key_exists($id = strtolower($id[0]),$this->_strs[$lang_id]) ? $this->_strs[$lang_id][$id] : $default;
111  }
112 
113  protected function macro($str,$type,&$tags,$callback){
114  if(
115  (strpos($str,'<' . ($tag = $this->namespace . ':' . $type)) !== false) &&
116  preg_match_all("/<$tag(?<attribs>(" . \Rsi\Str::ATTRIBUTES_PATTERN . ")*)>(?<str>.*?)<\\/$tag>/s",$str,$matches,PREG_SET_ORDER)
117  ) foreach($matches as $match) try{
118  $str = str_replace($match[0],call_user_func_array($callback,[$match['str'],\Rsi\Str::attributes($match['attribs']),&$tags]),$str);
119  }
120  catch(\Exception $e){
121  return $this->error($e,['macro' => $type]);
122  }
123  return $str;
124  }
125 
126  protected function preProcess($str,&$tags){
127  if($str){
128  $str = str_replace(array_keys($this->_escape),$this->_escape,$this->macro($str,self::MACRO_ESCAPE,$tags,function($str){
129  return str_replace($this->_unescape,array_keys($this->_unescape),$str);
130  }));
131  if(strpos($str,'<' . $this->namespace . ':') !== false){
132  $str = $this->macro($str,self::MACRO_SECTION,$tags,function($str,$attribs){
133  if(!array_key_exists($id = $attribs['id'] ?? $this->_prefix . $this->_sectionIndex,$this->_section)) $this->_section[$id] = $str;
134  $tag = $this->namespace . ':' . self::MACRO_SECTION;
135  return "<$tag id='$id'>" . $this->_sectionIndex++ . "</$tag>";
136  });
137  $str = $this->macro($str,self::MACRO_CACHE,$tags,function($str,$attribs,&$tags){
138  if(!($key = $attribs['key'] ?? null)) $key = md5($str);
139  elseif(preg_match_all('/\\[([\\w\\-]+)\\]/',$key,$matches,PREG_SET_ORDER)) foreach($matches as list($full,$tag)){
140  if(is_array($value = $this->value($tag,null,$tags))) $value = md5(serialize($value));
141  $key = str_replace($full,$value === null ? '' : $value,$key);
142  }
143  $key .= '-' . ($attribs['lang'] ?? $this->langId);
144  if(!is_numeric($ttl = $attribs['ttl'] ?? 0)) $ttl = $this->evaluate($ttl,null,$tags);
145  if(!array_key_exists($key,$this->_cache)) $this->_cache[$key] = $ttl ? $this->component('cache')->fetch($this->cacheKeyPrefix . $key) : false;
146  $tag = $this->namespace . ':' . self::MACRO_CACHE;
147  return "<$tag key='$key' ttl='$ttl'>" . ($this->_cache[$key] === false ? $str : '') . "</$tag>";
148  });
149  $str = $this->macro($str,self::MACRO_CONST,$tags,function($name){
150  return constant($name);
151  });
152  $str = $this->macro($str,self::MACRO_UNITS,$tags,function($tag,$attribs,&$tags){
153  $result = [];
154  foreach(\Rsi\Number::units($value = $tags[$tag] ?? 0,\Rsi\Record::explode($attribs['units'] ?? null,',','='),$value * ($attribs['tolerance'] ?? 0) / 100) as $unit => $count)
155  if($count || array_key_exists('empty',$attribs)) $result[] = compact('unit','count');
156  $tags[$attribs['tag'] ?? $tag . 'Units'] = $result;
157  return null;
158  });
159  $str = $this->macro($str,self::MACRO_LOCATION,$tags,function($str){
160  return htmlspecialchars($this->component('location')->rewrite($str));
161  });
162  $str = $this->macro($str,self::MACRO_URL,$tags,function($url,$attribs){
163  return $this->component('cache')->fetch($this->urlKeyPrefix . $url,$attribs['ttl'] ?? $this->urlTtl,function() use ($url){
164  $this->component('log')->debug("Loading external content from '$url'",__FILE__,__LINE__);
165  return preg_match('/^https?:\\/\\/(?!localhost|127\\.0\\.0\\.|\\[[0:]+1\\])/i',$url) ? file_get_contents($url) : $url;
166  });
167  });
168  $str = $this->macro($str,self::MACRO_RIGHT,$tags,function($str,$attribs){
169  return ($this->component('user')->authorized($attribs['right'] ?? null,$attribs['level'] ?? null) xor ($attribs['not'] ?? false)) ? $str : null;
170  });
171  }
172  }
173  return $str;
174  }
175 
176  protected function postProcess($str,&$tags){
177  if($str && (strpos($str,'<' . $this->namespace . ':') !== false)){
178  $str = $this->macro($str,self::MACRO_SECTION,$tags,function($str,$attribs,$tags){
179  $str = $this->_section[$id = $attribs['id']];
180  $this->_section[$id] = null;
181  return $this->trans($str,$tags);
182  });
183  $str = $this->macro($str,self::MACRO_FRAGMENT,$tags,function($str,$attribs){
184  $fragments = $this->session->fragments ?: [];
185  $fragments[$id = $attribs['id'] ?? 'fragment-' . \Rsi\Str::random()] = $str;
186  $this->session->fragments = $fragments;
187  return ($delay = $attribs['delay'] ?? null)
188  ? "<div class='fred-fragment' id='$id'><script>document.addEventListener('DOMContentLoaded',function(){" .
189  "setTimeout(function(){ fred.controller.fragment('#$id','$id') },$delay * 1000)" .
190  "},true)</script></div>"
191  : null;
192  });
193  $str = $this->macro($str,self::MACRO_CACHE,$tags,function($str,$attribs){
194  $key = $attribs['key'];
195  if(($this->_cache[$key] ?? false) !== false) return $this->_cache[$key];
196  if($ttl = $attribs['ttl'] ?? false) $this->component('cache')->save($this->cacheKeyPrefix . $key,$str,$ttl);
197  return $this->_cache[$key] = $str;
198  });
199  $str = $this->macro($str,self::MACRO_ROUTE,$tags,function($controller_name,$params){
200  $controller_name = explode('.',$controller_name);
201  if(array_key_exists('_',$params)){
202  foreach(($params['_'] == '*' ? array_keys($_GET) : explode(',',$params['_'])) as $key)
203  if(!array_key_exists($key,$params) && ($value = $this->component('request')->complex($key))) $params[$key] = $value;
204  unset($params['_']);
205  }
206  return htmlspecialchars($this->component('router')->reverse($controller_name[0],$controller_name[1] ?? null,$params));
207  });
208  $str = $this->macro($str,self::MACRO_SPACE,$tags,function($str){
209  return trim(preg_replace('/\\s*\\n\\s*/',"\n",preg_replace('/>\\s+</','><',$str)));
210  });
211  $str = $this->macro($str,self::MACRO_REPLACE,$tags,function($str,$attribs){
212  if(!array_key_exists('from',$attribs)) return $str;
213  if(substr($from = $attribs['from'],0,1) != '/') $from = '/' . preg_quote($from,'/') . '/';
214  return preg_replace($from,$attribs['to'] ?? null,$str);
215  });
216  $str = $this->macro($str,self::MACRO_IMAGE,$tags,function($url,$attribs){
217  return $this->component('cache')->fetch($this->imageKeyPrefix . $url . '?' . http_build_query($attribs),$attribs['ttl'] ?? $this->imageTtl,function() use ($url,$attribs){
218  unset($attribs['ttl']);
219  foreach(['width','height','background'] as $key) if(array_key_exists($key,$attribs) && ($attribs[$key] == 'auto')) $attribs[$key] = true;
220  return $this->component('html')->img($url,$attribs);
221  });
222  });
223  }
224  return $str;
225  }
226  /**
227  * Escape tag values (convert special characters to HTML entities).
228  * @param array $tags Tags with raw values (keys starting with '!' are not escaped - '!' is removed from key).
229  * @return array Escaped tags
230  */
231  public function escape($tags){
232  $escaped = [];
233  if($tags) foreach($tags as $tag => $value){
234  switch(substr($tag,0,1)){
235  case '!': $tag = substr($tag,1); //raw
236  case '@': break; //callback
237  default: if(!is_object($value)) $value = is_array($value) ? $this->escape($value) : ($value ? htmlspecialchars($value) : $value);
238  }
239  $escaped[$tag] = $value;
240  }
241  return $escaped;
242  }
243 
244  protected function value($tag,$params,&$tags){
245  if(\Rsi\Str::numeric($tag)) return $tag;
246  $value = $tags;
247  $found = true;
248  foreach(($keys = explode('.',$tag)) as $key){
249  if(is_object($value)) try{
250  $value = $this->escape([$value->$key])[0];
251  }
252  catch(\Exception $e){
253  $this->error("Property '$key' does not exists for tag '$tag'",__FILE__,__LINE__,compact('value'));
254  $found = false;
255  break;
256  }
257  elseif(!is_array($value) || !array_key_exists($key,$value)){
258  $found = false;
259  break;
260  }
261  else $value = $value[$key];
262  }
263  if($found) try{
264  return is_object($value) ? strval($value) : $value;
265  }
266  catch(\Exception $e){
267  $this->error("Could not convert object for tag '$tag' to string",__FILE__,__LINE__,compact('value'));
268  }
269  foreach($tags as $key => $value) if((substr($key,0,1) == '@') && !strcasecmp($keys[0],trim($key,'@!'))){
270  $value = [$keys[0] => call_user_func($value,$tag,$tags,$params)];
271  $tags += substr($key,1,1) == '!' ? $value : $this->escape($value);
272  return \Rsi\Record::iget($tags,$keys);
273  }
274  \Rsi\Record::set($tags,$keys,null);
275  return null;
276  }
277 
278  protected function evaluate($expr,$tag,&$tags){
279  $expr = strtr($expr,[$this->_prefix . true => '','?' => is_array($tag) ? implode(',',$tag) : $tag]);
280  foreach($tags as $key => $value) if(preg_match($mask = '/\\b' . preg_quote($tag = trim($key,'@!'),'/') . '\\b/',$expr)){
281  if(is_array($value = $this->value($tag,null,$tags))) $value = implode(',',$value);
282  elseif(preg_match('/^\\d{4}-\\d{2}-\\d{2}/',$value)) $value = "'$value'";
283  elseif(!is_numeric($value)) $value = $value ? 1 : 0;
284  $expr = preg_replace($mask,$value,$expr);
285  }
286  return \Rsi\Number::evaluate($expr);
287  }
288 
289  protected function formatSubstr($value,$start,$length = null){
290  return $length === null ? substr($value,$start) : substr($value,$start,$length);
291  }
292 
293  protected function formatReplace($value,$search,$replace){
294  return str_replace($search,$replace,$value);
295  }
296 
297  protected function formatRegex($value,$pattern,$replace){
298  return preg_replace($pattern,$replace,$value);
299  }
300 
301  protected function formatHash($value,$algo,$prefix = null,$suffix = null){
302  return hash($algo,$prefix . $value . $suffix);
303  }
304  /**
305  * Format a value.
306  * @param string $tag Tag name.
307  * @param mixed $value Tag value.
308  * @param string $params Formatting params.
309  * @param array $tags Other tags.
310  * @return string Formatted value.
311  */
312  protected function format($tag,$value,$params,&$tags){
313  foreach(explode('|',$params) as $format) if($format) try{
314  if(substr($format,0,1) == '=') $value = $this->evaluate(substr($format,1),$tag,$tags);
315  elseif(preg_match('/(\w+)\\((.*)\\)$/',$format,$match)){
316  $func_name = 'format' . ucfirst($match[1]);
317  $params = array_merge([$value],strlen($match[2]) ? array_map('urldecode',explode(',',$match[2])) : []);
318  if(method_exists($component = $this,$func_name) || method_exists($component = $this->component('local'),$func_name))
319  $value = call_user_func_array([$component,$func_name],$params);
320  else $value = call_user_func_array(
321  [$this->component('event'),'trigger'],
322  array_merge([self::EVENT_FORMAT . $match[1],$this],$params)
323  );
324  }
325  else{
326  $value = \Rsi\Str::transform($value,$format,$count);
327  if(!$count) $value = date($format,is_numeric($value) ? $value : strtotime($value));
328  }
329  }
330  catch(\Exception $e){
331  $value = $this->error($e,compact('tag','value','params'));
332  break;
333  }
334  return is_array($value) ? false : $value;
335  }
336  /**
337  * Replace a tag with its value.
338  * @param string $str String to replace tag in.
339  * @param string $full Full match for the tag (including brackets).
340  * @param int $index Position in string.
341  * @param string $type Type of tag (see TYPE_* constants).
342  * @param string $tag Name of the tag.
343  * @param string $operator Operator for the tag (e.g. '==').
344  * @param string $params Extra parameters in the tag.
345  * @param mixed $value Value for the tag.
346  * @param array $tags All available tags (values).
347  * @param array $strs Loaded sub-strings.
348  * @return string Original input string with the full tag replaced with its value.
349  */
350  protected function replace($str,$full,$index,$type,$tag,$operator,$params,$value,&$tags,&$strs){
351  $default = $replace = $raw = false;
352  if($type) switch($type){
353  case self::TYPE_TAG:
354  $tags[$tag] = substr($params,1);
355  $raw = true;
356  break;
357  case self::TYPE_OPTIONAL:
358  $default = null; //fall through
359  case self::TYPE_SUBSTR:
360  if(!in_array($tag,$strs)) $strs[] = $tag;
361  $replace = \Rsi\Str::transform($this->preProcess($this->getStr($tag,$default),$tags),$params = explode('|',$params));
362  $raw = !in_array(self::MACRO_ESCAPE,$params);
363  break;
364  case self::TYPE_TRUE:
365  if(substr($params,0,2) == '|=') $value = $this->evaluate(substr($params,2),$tag,$tags);
366  if($value) $replace = $this->_prefix . true;
367  $raw = true;
368  break;
369  case self::TYPE_FALSE:
370  if(substr($params,0,2) == '|=') $value = $this->evaluate(substr($params,2),$tag,$tags);
371  if(!$value) $replace = $this->_prefix . true;
372  $raw = true;
373  break;
374  case self::TYPE_COUNT:
375  $replace = count((array)$value);
376  $raw = true;
377  break;
378  case self::TYPE_SUM:
379  if(is_array($value)){
380  $params = explode('|',substr($params,1),2);
381  $replace = $this->format($tag,array_sum(\Rsi\Record::column($value,$params[0])),$params[1] ?? null,$tags) . $this->_prefix . true;
382  }
383  break;
384  case self::TYPE_ARRAY:
385  if($value && (is_array($value) || (is_numeric($value) && ($value > 0) && ($value = range(1,$value))))){
386  $replace = $this->_prefix . true;
387  $sub = str_replace(array_keys($this->_unescape),$this->_unescape,substr($params,1));
388  $i = 0; //index
389  $r = count($value); //reverse
390  foreach($value as $key => $record) $replace .=
391  $this->trans($sub,(is_array($record) ? $record : []) + ['v' => $record,'k' => $key,'i' => $i++,'r' => --$r] + $tags);
392  }
393  break;
394  case self::TYPE_DECODE:
395  $options = explode('|',substr($params,1));
396  $random = $value == '?' ? rand(0,count($options) - 1) : false;
397  foreach($options as $key => $option){
398  $i = strpos($option,':');
399  if($i === false) $replace = $option; //default
400  elseif($random === false ? \Rsi\Str::operator($value,$operator ?: '==',substr($option,0,$i)) : $key == $random){
401  $replace = substr($option,$i + 1);
402  break;
403  }
404  }
405  if(!\Rsi::nothing($replace)) $replace = $this->trans(str_replace(array_keys($this->_unescape),$this->_unescape,$replace),$tags) . $this->_prefix . true;
406  break;
407  case self::TYPE_JSON:
408  $replace = json_encode($value);
409  break;
410  case self::TYPE_HTML:
411  try{
412  $params = explode('|',substr($params,1));
413  $html = $this->component('html');
414  $html->escape = false; //escaping done here
415  $replace = call_user_func_array([$html,array_shift($params)],array_merge([$value],$params));
416  }
417  catch(\Exception $e){
418  $replace = $this->error($e,compact('tag','params'));
419  }
420  break;
421  case self::TYPE_RAW:
422  $replace = $this->format($tag,$value,$params,$tags);
423  break;
424  }
425  elseif($operator){
426  if(\Rsi\Str::operator(is_array($value) ? count($value) : str_replace($this->_unescape,array_keys($this->_unescape),$value ?? ''),$operator,$params)) $replace = $this->_prefix . true;
427  $raw = true;
428  }
429  elseif(!\Rsi::nothing($value)) //standard tag
430  $replace = $this->format($tag,$value,$params,$tags) . $this->_prefix . true;
431  return substr($str,0,$index) . ($raw ? $replace : str_replace($this->_unescape,array_keys($this->_unescape),$replace ?? '')) . substr($str,$index + strlen($full));
432  }
433  /**
434  * Elaborate conditional parts.
435  * @param string $str String with conditional parts.
436  * @return string String with elaborated conditional parts.
437  */
438  protected function brush($str){
439  while(($end = strpos($str,'}')) && (($start = strrpos($str,'{',$end - strlen($str))) !== false)) $str =
440  substr($str,0,$start) .
441  (strpos($replace = substr($str,$start + 1,$end - $start - 1),$this->_prefix . true) === false ? '' : str_replace($this->_prefix . true,'',$replace)) .
442  substr($str,$end + 1);
443  return $str;
444  }
445  /**
446  * Translate a string.
447  * @param string $str Basic string with tags.
448  * @param array $tags Tags to use when translating the string. Values are automatically escaped, except when the key starts
449  * with a '!'. For keys starting with a '@' the value is used as a callback function (only called when the tag is present
450  * in the string; use '@!' prefix for callback values that should not be escaped).
451  * @param array $steps Save translations steps when non empty.
452  * @return string Translated string.
453  */
454  protected function trans($str,$tags,&$steps = null){
455  if(!$str) return $str;
456  $count = $strs = [];
457  $step = 0;
458  $str = $this->preProcess($str,$tags);
459  if($steps) $steps['after pre-processing'] = $str;
460  while($str && preg_match_all($this->regex,$str,$matches,PREG_SET_ORDER | PREG_OFFSET_CAPTURE)){
461  foreach($strs as $id){
462  if(!array_key_exists($id,$count)) $count[$id] = 0;
463  elseif(++$count[$id] >= $this->maxNested){
464  $this->error("Sub-string '$id' nested more than {$this->maxNested} times",__FILE__,__LINE__,compact('str','tags'));
465  break 2;
466  }
467  }
468  $strs = [];
469  foreach(array_reverse($matches) as $match){ //0=full,1=type,2=tag,3=operator,4=params
470  if(substr($type = $match[1][0],0,1) == '{') $type = $this->types[substr($type,1,-1)] ?? null;
471  $tag = $match[2][0];
472  $params = $match[4][0];
473  $value = in_array($type,[self::TYPE_TAG,self::TYPE_OPTIONAL,self::TYPE_SUBSTR]) ? null : $this->value($tag,$params,$tags);
474  $str = $this->replace($str,$match[0][0],$match[0][1],$type,$tag,$match[3][0],$params,$value,$tags,$strs);
475  }
476  if($steps) $steps['step ' . ++$step] = $str;
477  }
478  return str_replace($this->_prefix . true,'',str_replace(array_keys($this->_unescape),$this->_unescape,$this->brush($str)));
479  }
480  /**
481  * Translate a string.
482  * @param string $str Basic string with tags.
483  * @param array $tags Tags to use when translating the string.
484  * @return string Translated string.
485  */
486  public function str($str,$tags = null){
487  if(!$str) return $str;
488  $tags = $this->escape(array_merge($this->_tags,$tags ?: []));
489  $steps = $this->_fred->debug ? ['original string' => $orig = $str] : null;
490  $str = $this->trans($str,$tags,$steps);
491  if($steps) $steps['before post-processing'] = $str;
492  $str = $this->postProcess($str,$tags);
493  if($steps && count($steps) >= $this->minLogSteps) $this->component('log')->debug(__CLASS__ . "::trans('$orig',...)",__FILE__,__LINE__,[
494  'steps' => $steps + ['result' => $str],
495  'tags' => $tags,
496  'prefix' => $this->_prefix,
497  'unescape' => $this->_unescape + [$this->_prefix . true => 'true']
498  ]);
499  return $str;
500  }
501  /**
502  * Translate a string by ID.
503  * @param string $id String ID.
504  * @param array $tags Tags to use when translating the string.
505  * @param string $default Default string when ID is not found.
506  * @return string Translated string.
507  */
508  public function id($id,$tags = null,$default = false){
509  return $this->str($this->getStr($id,$default),$tags);
510  }
511 
512  public function fragment($id){
513  $fragments = $this->session->fragments ?: [];
514  $fragment = $fragments[$id] ?? false;
515  unset($fragments[$id]);
516  $this->session->fragments = $fragments;
517  return $fragment;
518  }
519 
520  protected function getRegex(){
521  if(!$this->_regex){
522  $this->_regex = '/\\[(';
523  foreach($this->types as $type) $this->_regex .= preg_quote($type,'/') . '|';
524  $this->_regex .=
525  '\\{\\w+\\})?' . //type
526  '(\\w[\\w\\-\\/\\.]*\\w|\\w+)' . //tag
527  '(==|!=|>=?|<=?|=?%|-?\\*-?|!\\*|[\\/!]\\/)?' . //operator
528  '([^\\[\\]]*)' . //params
529  '\\]/';
530  }
531  return $this->_regex;
532  }
533 
534  protected function setTags($value){
535  $this->_tags = $value === false ? [] : array_merge($this->_tags,$value);
536  }
537 
538  protected function getTypes(){
539  if(!$this->_types){
540  $this->_types = [];
541  foreach($this->constants('TYPE_') as $name => $value) $this->_types[strtolower($name)] = $value;
542  }
543  return $this->_types;
544  }
545 
546  protected function _get($key){
547  return $this->id($key);
548  }
549 
550  public function __call($func_name,$params){
551  return $this->id($func_name,array_shift($params),$params ? array_shift($params) : false);
552  }
553 
554  public function __invoke($str,$tags = null){
555  return $this->str($str,$tags);
556  }
557 
558 }
Rsi\Fred\Trans\MACRO_LOCATION
const MACRO_LOCATION
Rewrite the location between the tags (pre processing).
Definition: Trans.php:39
Rsi\Fred\Trans\TYPE_DECODE
const TYPE_DECODE
Decode a value: [:intTag|1:foo|2:bar|default]. Set the value to '?' for a random selection.
Definition: Trans.php:19
Rsi\Fred\Trans\$cacheKeyPrefix
$cacheKeyPrefix
Definition: Trans.php:57
Rsi\Fred\Trans\EVENT_FORMAT
const EVENT_FORMAT
Definition: Trans.php:47
Rsi\Fred\Trans\$urlKeyPrefix
$urlKeyPrefix
Definition: Trans.php:56
Rsi\Fred\Trans\getTypes
getTypes()
Definition: Trans.php:538
Rsi\Fred\Trans\TYPE_JSON
const TYPE_JSON
Insert a JSON encoded value.
Definition: Trans.php:20
Rsi
Rsi\Fred\Trans\escape
escape($tags)
Escape tag values (convert special characters to HTML entities).
Definition: Trans.php:231
Rsi\Fred\Trans\id
id($id, $tags=null, $default=false)
Translate a string by ID.
Definition: Trans.php:508
Rsi\Fred\Trans
Translations component.
Definition: Trans.php:8
Rsi\Fred\Trans\$_langId
$_langId
Definition: Trans.php:59
Rsi\Fred\Trans\$namespace
$namespace
Definition: Trans.php:49
Rsi\Fred\Trans\postProcess
postProcess($str, &$tags)
Definition: Trans.php:176
Rsi\Fred\Trans\TYPE_SUBSTR
const TYPE_SUBSTR
Points to a sub-string: [@strId].
Definition: Trans.php:12
Rsi\Fred\Trans\value
value($tag, $params, &$tags)
Definition: Trans.php:244
Rsi\Fred\Trans\formatSubstr
formatSubstr($value, $start, $length=null)
Definition: Trans.php:289
Rsi\Fred\Trans\$urlTtl
$urlTtl
Default cache TTL for external content.
Definition: Trans.php:55
Rsi\Fred\Trans\$_tags
$_tags
Default tags to use with every translation.
Definition: Trans.php:63
Rsi\Fred\Trans\TYPE_COUNT
const TYPE_COUNT
Insert number of items in array: Array has [#arrayTag] items.
Definition: Trans.php:15
Rsi\Fred\Trans\MACRO_SECTION
const MACRO_SECTION
Define a section which content can be redefined later on (use the 'id' attribute)....
Definition: Trans.php:25
Rsi\Fred\Trans\MACRO_ROUTE
const MACRO_ROUTE
Create a (reverse) route for the controller name between the tags (attributes are params).
Definition: Trans.php:38
Rsi\Fred\Trans\$_filename
$_filename
Filename for the translations (asterisk will be replace by the langId).
Definition: Trans.php:60
Rsi\Fred\Trans\TYPE_HTML
const TYPE_HTML
Insert an HTML block: [&contentTag|htmlTag|params].
Definition: Trans.php:21
Rsi\Fred\Trans\formatHash
formatHash($value, $algo, $prefix=null, $suffix=null)
Definition: Trans.php:301
Rsi\Fred\Trans\getRegex
getRegex()
Definition: Trans.php:520
Rsi\Fred\Trans\TYPE_RAW
const TYPE_RAW
Insert value without inserting a conditional tag: [[~intTag],number].
Definition: Trans.php:22
Rsi\Fred\Trans\MACRO_SPACE
const MACRO_SPACE
Remove white-space between tags.
Definition: Trans.php:29
Rsi\Fred\Trans\preProcess
preProcess($str, &$tags)
Definition: Trans.php:126
Rsi\Thing\constants
constants($prefix=null)
Return all constants.
Definition: Thing.php:66
Rsi\Fred\Trans\MACRO_FRAGMENT
const MACRO_FRAGMENT
Replace the tag with a placeholder and replace it with the fragment later on (with.
Definition: Trans.php:27
Rsi\Fred\Trans\formatRegex
formatRegex($value, $pattern, $replace)
Definition: Trans.php:297
Rsi\Fred\Trans\MACRO_IMAGE
const MACRO_IMAGE
Generate an image tag for the image between the tags (the content is cached, set the 'ttl'.
Definition: Trans.php:40
Rsi\Fred\Trans\MACRO_ESCAPE
const MACRO_ESCAPE
Escape the text between these tags (pre processing).
Definition: Trans.php:24
Rsi\Fred\Log\INFO
const INFO
Informational message.
Definition: Log.php:14
Rsi\Fred\Trans\$logPrio
$logPrio
Prio for translation related errors (invalid tags, format, etc).
Definition: Trans.php:51
Rsi\Fred\Trans\getStrs
getStrs($lang_id)
Get all strings.
Definition: Trans.php:97
Rsi\Fred\Trans\$maxNested
$maxNested
Maximum number of times a sub-string may be nested.
Definition: Trans.php:50
Rsi\Fred\Component
Basic component class.
Definition: Component.php:8
Rsi\Fred\Trans\replace
replace($str, $full, $index, $type, $tag, $operator,$params, $value, &$tags, &$strs)
Replace a tag with its value.
Definition: Trans.php:350
Rsi\Fred\Trans\TYPE_OPTIONAL
const TYPE_OPTIONAL
Optional sub-string (defaults to empty; no error): [?strId].
Definition: Trans.php:11
Rsi\Fred\Trans\TYPE_TRUE
const TYPE_TRUE
Show section when tag value is true: {[tag]show when tag is not empty}.
Definition: Trans.php:13
Rsi\Fred\Trans\init
init()
Definition: Trans.php:74
Rsi\Fred\Component\component
component($name)
Get a component (local or default).
Definition: Component.php:81
Rsi\Fred\Trans\MACRO_CACHE
const MACRO_CACHE
Cache the (processed) text (set the 'ttl' tag to use the main cache; use the 'key'.
Definition: Trans.php:36
Rsi\Fred\Trans\$_types
$_types
Definition: Trans.php:71
Rsi\Fred\Trans\str
str($str, $tags=null)
Translate a string.
Definition: Trans.php:486
Rsi\Fred\Trans\MACRO_UNITS
const MACRO_UNITS
Split the value of the tag name into units, so it can be used as an array (units in desired.
Definition: Trans.php:31
Rsi\Fred\Trans\MACRO_CONST
const MACRO_CONST
Replace the text with the value of the constant between the tags (pre processing).
Definition: Trans.php:35
Rsi\Fred\Trans\$minLogSteps
$minLogSteps
Only log translation steps with more than this number of steps.
Definition: Trans.php:52
Rsi\Fred\Trans\$_section
$_section
Definition: Trans.php:65
Rsi\Fred\Trans\langs
langs()
Available languages.
Definition: Trans.php:84
Rsi\Fred\Trans\getStr
getStr($id, $default=false)
Basic string getter.
Definition: Trans.php:106
Rsi\Fred\Trans\__call
__call($func_name, $params)
Definition: Trans.php:550
Rsi\Fred\Trans\fragment
fragment($id)
Definition: Trans.php:512
Rsi\Fred\Trans\formatReplace
formatReplace($value, $search, $replace)
Definition: Trans.php:293
Rsi\Thing\publish
publish($property, $visibility=self::READABLE)
Publish a property (or hide it again).
Definition: Thing.php:28
Rsi\Fred\Trans\$imageKeyPrefix
$imageKeyPrefix
Definition: Trans.php:54
Rsi\Fred\Trans\MACRO_REPLACE
const MACRO_REPLACE
Replace the text in attribute 'from' (regex) to 'to'.
Definition: Trans.php:30
Rsi\Fred\Trans\$_regex
$_regex
Definition: Trans.php:72
Rsi\Fred\Trans\MACRO_RIGHT
const MACRO_RIGHT
Definition: Trans.php:44
Rsi\Fred\Trans\TYPE_FALSE
const TYPE_FALSE
Show section when tag value is false: {[!tag]show when tag is empty}.
Definition: Trans.php:14
Rsi\Fred\Trans\$_prefix
$_prefix
Definition: Trans.php:61
Rsi\Fred\Trans\trans
trans($str, $tags, &$steps=null)
Translate a string.
Definition: Trans.php:454
Rsi\Fred\Trans\TYPE_TAG
const TYPE_TAG
Add a tag to to the tags: [+tag|value].
Definition: Trans.php:10
Rsi\Fred\Trans\macro
macro($str, $type, &$tags, $callback)
Definition: Trans.php:113
Rsi\Fred\Trans\setTags
setTags($value)
Definition: Trans.php:534
Rsi\Fred\Trans\$imageTtl
$imageTtl
Default cache TTL for image tag generation.
Definition: Trans.php:53
Rsi\Fred\Trans\brush
brush($str)
Elaborate conditional parts.
Definition: Trans.php:438
Rsi\Fred\Trans\$_unescape
$_unescape
Definition: Trans.php:68
Rsi\Fred\Trans\format
format($tag, $value, $params, &$tags)
Format a value.
Definition: Trans.php:312
Rsi\Fred\Trans\$_cache
$_cache
Definition: Trans.php:69
Rsi\Fred\Trans\$_escape
$_escape
Definition: Trans.php:67
Rsi\Fred\Trans\__invoke
__invoke($str, $tags=null)
Definition: Trans.php:554
Rsi\Fred\Trans\TYPE_SUM
const TYPE_SUM
Sum of an array column: [=array|column|formatting].
Definition: Trans.php:16
Rsi\Fred\Trans\TYPE_ARRAY
const TYPE_ARRAY
Repeat section for each item (item aded to tags, including k = key, v = value - when record is.
Definition: Trans.php:17
Rsi\Fred\Trans\$_sectionIndex
$_sectionIndex
Definition: Trans.php:66
Rsi\Fred\Trans\$_strs
$_strs
Definition: Trans.php:62
Rsi\Fred\Trans\error
error($message, $file=null, $line_no=null, $context=null)
Definition: Trans.php:88
Rsi\Fred\Trans\MACRO_URL
const MACRO_URL
Load external content from the address between the tags (the content is cached, set the 'ttl'.
Definition: Trans.php:42
Rsi\Fred\Exception
Definition: Exception.php:5
Rsi\Fred\Trans\evaluate
evaluate($expr, $tag, &$tags)
Definition: Trans.php:278
Rsi\Fred\Trans\_get
_get($key)
Default getter if no specific setter is defined, and the property is also not published (readable).
Definition: Trans.php:546
Rsi\Fred
Definition: Alive.php:3