id );
$where[] = array( "word_key LIKE CONCAT( ?, '%' )", $prefix );
if ( $alsoSearchDefault )
{
$where[] = array( "word_custom LIKE CONCAT( '%', ?, '%' ) OR ( word_custom IS NULL AND word_default LIKE CONCAT( '%', ?, '%' ) )", $query, $query );
}
else
{
$where[] = array( "word_custom LIKE CONCAT( '%', ?, '%' )", $query );
}
foreach ( \IPS\Db::i()->select( '*', 'core_sys_lang_words', $where ) as $row )
{
$return[ mb_substr( $row['word_key'], mb_strlen( $prefix ) ) ] = $this->get( $row['word_key'] );
}
return $return;
}
/**
* BCP 47
*
* @return string
* @see BCP 47 - Tags for Identifying Languages
*/
public function bcp47()
{
if ( preg_match( '/^([a-z]{2})[-_]([a-z]{2})(.utf-?8)?$/i', $this->short, $matches ) )
{
return mb_strtolower( $matches[1] ) . '-' . mb_strtoupper( $matches[2] );
}
else
{
return mb_substr( $this->short, 0, 2 );
}
}
/* !Node */
/**
* @brief Order Database Column
*/
public static $databaseColumnOrder = 'order';
/**
* @brief Node Title
*/
public static $nodeTitle = 'menu__core_languages_languages';
/**
* @brief ACP Restrictions
*/
protected static $restrictions = array(
'app' => 'core',
'module' => 'languages',
'all' => 'lang_packs'
);
/**
* @brief [Node] Show forms modally?
*/
public static $modalForms = TRUE;
/**
* Search
*
* @param string $column Column to search
* @param string $query Search query
* @param string|null $order Column to order by
* @param mixed $where Where clause
* @return array
*/
public static function search( $column, $query, $order=NULL, $where=array() )
{
if ( $column === '_title' )
{
$column = 'lang_title';
}
if ( $order === '_title' )
{
$order = 'lang_title';
}
return parent::search( $column, $query, $order, $where );
}
/**
* [Node] Does the currently logged in user have permission to add children for this node?
*
* @return bool
*/
public function canAdd()
{
return FALSE;
}
/**
* [Node] Does the currently logged in user have permission to delete this node?
*
* @return bool
*/
public function canDelete()
{
return !$this->default;
}
/**
* [Node] Add/Edit Form
*
* @param \IPS\Helpers\Form $form The form
* @return void
*/
public function form( &$form )
{
$form->add( new \IPS\Helpers\Form\Text( 'lang_title', $this->title, TRUE, array( 'maxLength' => 255 ) ) );
$this->localeField( $form, $this->id ? $this->short : 'en_US' );
$form->add( new \IPS\Helpers\Form\Select( 'lang_isrtl', $this->isrtl, FALSE, array( 'options' => array( FALSE => 'lang_isrtl_left', TRUE => 'lang_isrtl_right' ) ) ) );
if ( !$this->default )
{
$form->add( new \IPS\Helpers\Form\YesNo( 'lang_default', $this->default, FALSE ) );
}
}
/**
* Add locale field to form
*
* @param \IPS\Helpers\Form $form The form
* @return void
*/
public static function localeField( &$form, $current='en_US' )
{
$commonLocales = json_decode( file_get_contents( \IPS\ROOT_PATH . '/system/Lang/locales.json' ), TRUE );
natcasesort( $commonLocales );
foreach ( $commonLocales as $k => $v )
{
try
{
static::validateLocale( $k );
}
catch ( \InvalidArgumentException $e )
{
unset( $commonLocales[ $k ] );
}
}
if ( !empty( $commonLocales ) )
{
$form->add( new \IPS\Helpers\Form\Select( 'lang_short', array_key_exists( preg_replace( '/^(.+?)\..+?$/', '$1', $current ), $commonLocales ) ? preg_replace( '/^(.+?)\..+?$/', '$1', $current ) : 'x', TRUE, array(
'options' => array_merge( $commonLocales, array( 'x' => \IPS\Member::loggedIn()->language()->addToStack('lang_short_other') ) ),
'toggles' => array( 'x' => array( 'locale_custom' ) ),
'parse' => 'raw'
), '\IPS\Lang::validateLocale' ) );
}
else
{
$form->hiddenValues['lang_short'] = 'x';
}
$form->add( new \IPS\Helpers\Form\Text( 'lang_short_custom', !in_array( $current, $commonLocales ) ? $current : NULL, FALSE, array( 'placeholder' => 'en_US' ), '\IPS\Lang::validateLocale', NULL, NULL, 'locale_custom' ) );
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( $values )
{
if( isset( $values['lang_short_custom'] ) )
{
if ( !isset($values['lang_short']) OR $values['lang_short'] === 'x' )
{
$values['lang_short'] = $values['lang_short_custom'];
}
unset( $values['lang_short_custom'] );
}
if( isset( $values['lang_short'] ) )
{
$currentLocale = setlocale( LC_ALL, '0' );
foreach ( array( "{$values['lang_short']}.UTF-8", "{$values['lang_short']}.UTF8" ) as $l )
{
$test = setlocale( LC_ALL, $l );
if ( $test !== FALSE )
{
$values['lang_short'] = $l;
break;
}
}
static::restoreLocale( $currentLocale );
}
foreach ( $values as $k => $v )
{
$this->_data[ $k ] = $v;
$this->changed[ mb_substr( $k, 5 ) ] = $v;
}
if( isset( $values['lang_default'] ) and $values['lang_default'] )
{
$this->enabled = TRUE;
\IPS\Db::i()->update( 'core_sys_lang', array( 'lang_default' => 0 ) );
unset( \IPS\Data\Store::i()->languages );
}
return $values;
}
/**
* Get title
*
* @return string
*/
public function get__title()
{
return $this->title;
}
/**
* Get Icon
*
* @return string
* @note Works on Unix systems. Partial support for Windows systems.
*/
public function get__icon()
{
return "ipsFlag ipsFlag-{$this->getCountry()}";
}
/**
* Return country code
*
* @return string
*/
protected function getCountry()
{
/* We may need to remap some entries for Windows */
if( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) === 'win' )
{
/* If it's just "english", consider that US */
if( mb_strtolower( $this->short ) === 'english' )
{
return 'us';
}
elseif( mb_strtolower( $this->short ) === 'german' )
{
return 'de';
}
if( mb_strpos( $this->short, '_' ) !== FALSE )
{
$pieces = explode( '_', $this->short );
if( mb_strpos( mb_strtolower( $pieces[0] ), 'chinese' ) === 0 )
{
return 'cn';
}
elseif( mb_strtolower( $pieces[0] ) === 'english' AND mb_strtolower( $pieces[1] ) === 'uk' )
{
return 'gb';
}
}
}
return ( mb_strpos( $this->short, '_' ) !== FALSE ) ? mb_strtolower( mb_substr( $this->short, mb_strpos( $this->short, '_' ) + 1, 2 ) ) : mb_strtolower( mb_substr( $this->short, 0, 2 ) );
}
/**
* Get enabled
*
* @return bool
*/
public function get__enabled()
{
return (bool) $this->enabled;
}
/**
* Get locked
*
* @return bool
*/
public function get__locked()
{
return (bool) $this->default;
}
/**
* [Node] Get buttons to display in tree
* Example code explains return value
*
* @code
array(
array(
'icon' => 'plus-circle', // Name of FontAwesome icon to use
'title' => 'foo', // Language key to use for button's title parameter
'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
'class' => 'modalLink' // CSS Class to use on link (Optional)
),
... // Additional buttons
);
* @endcode
* @param string $url Base URL
* @param bool $subnode Is this a subnode?
* @return array
*/
public function getButtons( $url, $subnode=FALSE )
{
$buttons = array();
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' ) and ( !isset( \IPS\Request::i()->cookie['vle_editor'] ) or \IPS\Request::i()->cookie['vle_editor'] == 0 ) )
{
$buttons['translate'] = array(
'icon' => 'globe',
'title' => 'lang_translate',
'link' => \IPS\Http\Url::internal( "app=core&module=languages&controller=languages&do=translate&id={$this->_id}" ),
);
}
$buttons = array_merge( $buttons, parent::getButtons( $url, $subnode ) );
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' ) )
{
$buttons['upload'] = array(
'icon' => 'upload',
'title'=> 'upload_new_version',
'link' => \IPS\Http\Url::internal( "app=core&module=languages&controller=languages&do=uploadNewVersion&id={$this->_id}" ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->get('upload_new_version') )
);
}
if ( $this->canEdit() )
{
$buttons['download'] = array(
'icon' => 'download',
'title' => 'download',
'link' => \IPS\Http\Url::internal( "app=core&module=languages&controller=languages&do=download&id={$this->_id}" ),
);
}
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_edit' ) )
{
$buttons[] = array(
'icon' => 'user',
'title' => 'language_set_members',
'link' => $url->setQueryString( array( 'do' => 'setMembers', 'id' => $this->default ? 0 : $this->_id ) ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => $this->_title )
);
}
if ( \IPS\IN_DEV )
{
$buttons['devimport'] = array(
'icon' => 'cogs',
'title' => 'lang_dev_import',
'link' => \IPS\Http\Url::internal( "app=core&module=languages&controller=languages&do=devimport&id={$this->_id}" ),
);
}
return $buttons;
}
/* !ActiveRecord */
/**
* @brief Multiton Store
*/
protected static $multitons;
/**
* @brief Default Values
*/
protected static $defaultValues = array(
'lang_id' => 0,
'lang_short' => 'en_US',
'lang_title' => 'English (USA)',
'lang_default' => TRUE,
'lang_isrtl' => FALSE,
'lang_protected'=> FALSE,
'lang_order' => 0,
'lang_enabled' => TRUE
);
/**
* @brief Database Table
*/
public static $databaseTable = 'core_sys_lang';
/**
* @brief Database Prefix
*/
public static $databasePrefix = 'lang_';
/**
* @brief Has been initialized?
*/
protected $_initialized = FALSE;
/**
* Set words
*
* @return void
*/
public function languageInit()
{
/* Only initialize once */
if( $this->_initialized === TRUE )
{
return;
}
$this->_initialized = TRUE;
/* Set locale data */
$this->set_short( $this->short );
/* Get values from developer files */
if ( \IPS\IN_DEV or \IPS\Settings::i()->theme_designers_mode )
{
/* Apps and plugins */
if ( \IPS\IN_DEV )
{
try
{
foreach ( \IPS\Application::applications() as $app )
{
if ( file_exists( \IPS\ROOT_PATH . "/applications/{$app->directory}/dev/lang.php" ) )
{
require \IPS\ROOT_PATH . "/applications/{$app->directory}/dev/lang.php";
$this->words = array_merge( $this->words, $lang );
}
}
}
catch( \UnexpectedValueException $ex )
{
\IPS\Output\System::i()->error( $ex->getMessage(), 500 );
}
foreach ( \IPS\Plugin::plugins() as $plugin )
{
if ( file_exists( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/lang.php" ) )
{
require \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/lang.php";
$this->words = array_merge( $this->words, $lang );
}
}
}
/* Themes */
if ( \IPS\Settings::i()->theme_designers_mode )
{
foreach ( \IPS\Theme::themes() as $theme )
{
if ( file_exists( \IPS\ROOT_PATH . '/themes/' . $theme->id . '/lang.php' ) )
{
require \IPS\ROOT_PATH . '/themes/' . $theme->id . '/lang.php';
$this->words = array_merge( $this->words, $lang );
}
}
}
/* Allow custom strings to override the default strings */
foreach( \IPS\Db::i()->select( 'word_key, word_default, word_custom', 'core_sys_lang_words', array( 'lang_id=? and word_export=?', $this->id, '0' ) ) as $bit )
{
$this->words[ $bit['word_key'] ] = $bit['word_custom'] ?: $bit['word_default'];
}
}
}
/**
* Set locale data
*
* @param string $val Locale
* @return void
*/
public function set_short( $val )
{
$oldLocale = setlocale( LC_ALL, '0' );
setlocale( LC_ALL, $val );
/* Some locales in some PHP versions break things drastically */
if( in_array( 'ips\\db\\_select', get_declared_classes() ) )
{
setlocale( LC_CTYPE, 'en_US.UTF-8' );
}
$this->locale = localeconv();
foreach( explode( ";", $oldLocale ) as $locale )
{
if( mb_strpos( $locale, '=' ) !== FALSE )
{
$parts = explode( "=", $locale );
if( in_array( $parts[0], array( 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME' ) ) )
{
setlocale( constant( $parts[0] ), $parts[1] );
}
}
else
{
setlocale( LC_ALL, $locale );
}
}
}
/**
* Save Changed Columns
*
* @return void
*/
public function save()
{
parent::save();
unset( \IPS\Data\Store::i()->languages );
}
/**
* Delete Record
*
* @return void
*/
public function delete()
{
parent::delete();
\IPS\Db::i()->delete( 'core_sys_lang_words', array( 'lang_id=?', $this->id ) );
unset( \IPS\Data\Store::i()->languages );
}
/**
* Parse output and replace language keys
*
* @param string $output Unparsed
* @return void
*/
public function parseOutputForDisplay( &$output )
{
/* Do we actually have any? */
if( !count( $this->outputStack ) )
{
return;
}
/* Parse out lang */
$keys = array();
foreach( $this->outputStack as $word => $values )
{
if( !isset( $this->words[ $values['key'] ] ) )
{
$keys[] = "'" . \IPS\Db::i()->real_escape_string( $values['key'] ) . "'";
}
}
if( !$this->wordsLoaded === TRUE AND count( $keys ) and !\IPS\IN_DEV )
{
foreach( \IPS\Db::i()->select( 'word_key, word_default, word_custom', 'core_sys_lang_words', array( "lang_id=? AND word_key IN(" . implode( ",", $keys ) . ") and word_js=0", $this->id ) ) as $row )
{
$this->words[ $row['word_key'] ] = $row['word_custom'] ?: $row['word_default'];
}
foreach( $this->outputStack as $word => $values )
{
if( !isset( $this->words[ $values['key'] ] ) )
{
if( isset( $values['options']['returnBlank'] ) AND $values['options']['returnBlank'] === TRUE )
{
$this->words[ $values['key'] ] = '';
}
else
{
$this->words[ $values['key'] ] = $values['key'];
}
}
}
}
/* Adjust for VLE */
if ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] and \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' ) )
{
$this->originalWords = $this->words;
}
if ( isset( \IPS\Request::i()->cookie['vle_keys'] ) and \IPS\Request::i()->cookie['vle_keys'] and \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' ) )
{
$this->words = array_combine( array_keys( $this->words ), array_keys( $this->words ) );
}
$this->outputStack = array_reverse( $this->outputStack );
$this->replaceWords( $output );
}
/**
* Emails require some additional work on top of replacing the language stack
*
* @param string $output Unparsed
* @return void
*/
public function parseEmail( &$output )
{
$this->parseOutputForDisplay( $output );
$output = $this->stripVLETags( $output );
$dir = ( $this->isrtl ) ? 'rtl' : 'ltr';
if ( mb_stristr( $output, '{dir}' ) )
{
$output = preg_replace( '#(| ]+?)>#i', $output, $matches, PREG_SET_ORDER );
foreach( $matches as $match )
{
if ( mb_stristr( $match[2], '{dir}' ) )
{
$parsed = str_replace( '{dir}', $dir, $match[0] );
$output = str_replace( $match[0], $parsed, $output );
}
}
}
}
/**
* Strip VLE tags, useful for AJAX responses where you can't edit the string anyways
*
* @param string $output The output string
* @return string
*/
public function stripVLETags( $output )
{
if( !is_array( $output ) )
{
return preg_replace( "/#VLE#.+?#\!#\[(.+?)\]#\!##/", "$1", $output );
}
$replacement = array();
foreach ( $output as $key => $value )
{
$replacement[ $key ] = $this->stripVLETags( $value );
}
return $replacement;
}
/**
* Insert tags into long words
*
* @deprecated This is left here to prevent breaking third party addons, but no longer breaks words.
* @note You should use a container div with classes ipsContained and ipsType_break instead.
* Often you may want to also use ipsTruncate and ipsTruncate_line on an inline element (i.e. an tag) wrapping the text as well to truncate.
* @param string $data The string
* @return string
*/
public static function wordbreak( $data )
{
return $data;
}
/**
* Replace values in array recursively
*
* @param string $find The string to find
* @param string $replace The string to replace with
* @param string|array $haystack The subject
*
* @return string|array
*/
public static function replace( $find, $replace, $haystack )
{
/* Reduce the number of str_replace with arrays */
static $replaceTable = array();
if ( ! is_array( $haystack ) )
{
$hash = md5( json_encode($find) . json_encode($replace) . $haystack );
if ( isset( $replaceTable[ $hash ] ) )
{
return $replaceTable[ $hash ];
}
else
{
$output = str_replace( $find, $replace, $haystack );
$replaceTable[ $hash ] = $output;
return $output;
}
}
$replacement = array();
foreach ( $haystack as $key => $value )
{
$replacement[ $key ] = static::replace( $find, $replace, $value );
}
return $replacement;
}
/**
* Parse the output stack
*
* @param string $output Unparsed
* @return void
*/
public function replaceWords( &$output )
{
/* It's possible to call this method and not pass in any content - it's a waste of resources to run replacements on an empty string */
if( !$output )
{
return;
}
$replacements = array();
foreach ( $this->outputStack as $key => $values )
{
if ( isset( $values[ 'options' ][ 'returnBlank' ] ) AND $values[ 'options' ][ 'returnBlank' ] === true AND ( !isset( $this->words[ $values[ 'key' ] ] ) OR !$this->words[ $values[ 'key' ] ] ) )
{
$replacements[ $key ] = "";
continue;
}
else
{
if ( isset( $this->words[ $values[ 'key' ] ] ) )
{
$replacement = $this->words[ $values[ 'key' ] ];
/* Parse URLs */
if ( mb_strpos( $replacement, "{external" ) !== false )
{
$replacement = preg_replace_callback(
"/{external\.(.+?)}/",
function ( $matches )
{
return \IPS\Http\Url::ips( 'docs/' . $matches[ 1 ] );
},
$replacement
);
}
if ( mb_strpos( $replacement, "{internal" ) !== false )
{
$replacement = preg_replace_callback(
"/{internal\.([a-zA-Z]+?)\.(.+?)\.(.+?)}/",
function ( $matches )
{
return \IPS\Http\Url::internal( $matches[ 2 ], $matches[ 1 ], $matches[ 3 ] );
},
$replacement
);
$replacement = preg_replace_callback(
"/{internal\.([a-zA-Z]+?)\.(.+?)}/",
function ( $matches )
{
return \IPS\Http\Url::internal( $matches[ 2 ], $matches[ 1 ] );
},
$replacement
);
$replacement = preg_replace_callback(
"/{internal\.(.+?)}/",
function ( $matches )
{
return \IPS\Http\Url::internal( $matches[ 1 ] );
},
$replacement
);
}
$sprintf = array();
if ( isset( $values[ 'options' ][ 'flipsprintf' ] ) AND $values[ 'options' ][ 'flipsprintf' ] === true )
{
if ( isset( $values[ 'options' ][ 'sprintf' ] ) and $values[ 'options' ][ 'sprintf' ] !== null )
{
$replacement = $values[ 'options' ][ 'sprintf' ];
$values[ 'options' ][ 'sprintf' ] = array( $this->words[ $values[ 'key' ] ] );
}
if ( isset( $values[ 'options' ][ 'htmlsprintf' ] ) and $values[ 'options' ][ 'htmlsprintf' ] !== null )
{
$replacement = $values[ 'options' ][ 'htmlsprintf' ];
$values[ 'options' ][ 'htmlsprintf' ] = array( $this->words[ $values[ 'key' ] ] );
}
}
if ( isset( $values[ 'options' ][ 'sprintf' ] ) and $values[ 'options' ][ 'sprintf' ] !== null )
{
$sprintf = array_map(
function ( $val ) use ( $replacement )
{
return htmlspecialchars( trim( $val ), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', false );
},
( is_array(
$values[ 'options' ][ 'sprintf' ]
) ? $values[ 'options' ][ 'sprintf' ] : explode( ',', $values[ 'options' ][ 'sprintf' ] ) )
);
}
if ( isset( $values[ 'options' ][ 'htmlsprintf' ] ) and $values[ 'options' ][ 'htmlsprintf' ] !== null )
{
$sprintf = array_merge(
$sprintf,
( is_array(
$values[ 'options' ][ 'htmlsprintf' ]
) ? $values[ 'options' ][ 'htmlsprintf' ] : explode(
',',
$values[ 'options' ][ 'htmlsprintf' ]
) )
);
}
if ( count( $sprintf ) )
{
try
{
$replacement = vsprintf( $replacement, $sprintf );
/* Without IN_DEV we suppress warnings, so we need to verify if the return was FALSE */
if( $replacement === FALSE )
{
throw new \ErrorException;
}
}
catch ( \ErrorException $e )
{
// If there's the wrong number of parameters because the translator's done it wrong, we can just use empty strings for replacements
}
}
if ( !empty( $values[ 'options' ][ 'pluralize' ] ) )
{
$replacement = $this->pluralize(
$replacement,
$values[ 'options' ][ 'pluralize' ]
);
}
}
else
{
$replacement = $values[ 'key' ];
}
}
if ( isset( $values[ 'options' ][ 'wordbreak' ] ) )
{
$replacement = \IPS\Lang::wordbreak( $replacement );
}
if ( isset( $values[ 'options' ][ 'ucfirst' ] ) )
{
$replacement = mb_strtoupper( mb_substr( $replacement, 0, 1 ) ) . mb_substr( $replacement, 1 );
}
if ( isset( $values[ 'options' ][ 'strtoupper' ] ) )
{
$replacement = mb_strtoupper( $replacement );
}
if ( isset( $values[ 'options' ][ 'strtolower' ] ) )
{
$replacement = mb_strtolower( $replacement );
}
if ( isset( $values[ 'options' ][ 'json' ] ) )
{
$replacement = mb_substr( json_encode( $replacement ), 1, -1 );
}
if ( isset( $values[ 'options' ][ 'striptags' ] ) )
{
$replacement = strip_tags( $replacement );
}
if ( isset( $values[ 'options' ][ 'escape' ] ) )
{
$replacement = htmlspecialchars( $replacement, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', false );
}
if ( isset( $values['options']['escape'] ) and isset( $values['options']['json'] ) )
{
/* Some browsers treat ' as a literal ' which can break things when used in JSON
A similar issue occurrs in IPS\gallery\Image::json() */
$replacement = str_replace( "'", "'", $replacement );
}
/* Add VLE tags */
if ( $values[ 'vle' ] and $replacement and isset( \IPS\Request::i(
)->cookie[ 'vle_editor' ] ) and \IPS\Request::i()->cookie[ 'vle_editor' ] and \IPS\Member::loggedIn(
)->hasAcpRestriction( 'core', 'languages', 'lang_words' )
)
{
$replacement = "#VLE#{$values['key']}#!#[{$replacement}]#!##";
}
if ( isset( $values[ 'options' ][ 'returnInto' ] ) )
{
$replacement = sprintf( $values[ 'options' ][ 'returnInto' ], $replacement );
}
$replacements[ $key ] = $replacement;
}
/* We do this 4 times in case a replacement contains another replacement, etc. */
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
$output = static::replace( array_keys( $replacements ), array_values( $replacements ), $output );
return $output;
}
} |