2109 lines
56 KiB
PHP
2109 lines
56 KiB
PHP
<?php
|
|
/**
|
|
* @brief Language Class
|
|
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
|
* @copyright (c) Invision Power Services, Inc.
|
|
* @license https://www.invisioncommunity.com/legal/standards/
|
|
* @package Invision Community
|
|
* @since 18 Feb 2013
|
|
*/
|
|
|
|
namespace IPS;
|
|
|
|
/* To prevent PHP errors (extending class does not exist) revealing path */
|
|
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
|
{
|
|
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Language Class
|
|
*/
|
|
class _Lang extends \IPS\Node\Model
|
|
{
|
|
/* !Lang - Static */
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Caches
|
|
* @note Defined cache keys will be cleared automatically as needed
|
|
*/
|
|
protected $caches = array( 'languages', 'listFormats', 'updatecount_languages', 'shortFormats' );
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Database ID Fields
|
|
*/
|
|
protected static $databaseIdFields = array('lang_id', 'lang_marketplace_id');
|
|
|
|
/**
|
|
* @brief Have fetched all?
|
|
*/
|
|
protected static $gotAll = FALSE;
|
|
|
|
/**
|
|
* @brief Default language ID
|
|
*/
|
|
protected static $defaultLanguageId = NULL;
|
|
|
|
/**
|
|
* @brief Output lang stack
|
|
*/
|
|
public $outputStack = array();
|
|
|
|
/**
|
|
* @brief lang key salt
|
|
*/
|
|
protected static $outputSalt = NULL;
|
|
|
|
/**
|
|
* @brief Have all the words been loaded?
|
|
*/
|
|
protected $wordsLoaded = FALSE;
|
|
|
|
/**
|
|
* Load Record
|
|
*
|
|
* @see \IPS\Db::build
|
|
* @param int|string $id ID
|
|
* @param string $idField The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
|
|
* @param mixed $extraWhereClause Additional where clause(s) (see \IPS\Db::build for details)
|
|
* @return static
|
|
* @throws \InvalidArgumentException
|
|
* @throws \OutOfRangeException
|
|
*/
|
|
public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
|
|
{
|
|
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation == 'front' AND $idField === NULL AND $extraWhereClause === NULL )
|
|
{
|
|
$languages = static::languages();
|
|
|
|
if ( !isset( $languages[ $id ] ) )
|
|
{
|
|
throw new \OutOfRangeException;
|
|
}
|
|
|
|
$languages[ $id ]->languageInit();
|
|
return $languages[ $id ];
|
|
}
|
|
|
|
$result = parent::load( $id, $idField, $extraWhereClause );
|
|
$result->languageInit();
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get data store
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function getStore()
|
|
{
|
|
if ( !isset( \IPS\Data\Store::i()->languages ) )
|
|
{
|
|
\IPS\Data\Store::i()->languages = iterator_to_array( \IPS\Db::i()->select( '*', 'core_sys_lang', NULL, 'lang_order' )->setKeyField('lang_id') );
|
|
}
|
|
|
|
return \IPS\Data\Store::i()->languages;
|
|
}
|
|
|
|
/**
|
|
* Languages
|
|
*
|
|
* @param null|\IPS\Db\Select $iterator Select iterator
|
|
* @return array
|
|
*/
|
|
public static function languages( $iterator=NULL )
|
|
{
|
|
if ( !static::$gotAll )
|
|
{
|
|
if( $iterator === NULL )
|
|
{
|
|
$rows = static::getStore();
|
|
}
|
|
else
|
|
{
|
|
$rows = iterator_to_array( $iterator );
|
|
}
|
|
|
|
foreach( $rows as $id => $lang )
|
|
{
|
|
if ( $lang['lang_default'] )
|
|
{
|
|
static::$defaultLanguageId = $lang['lang_id'];
|
|
}
|
|
static::$multitons[ $id ] = static::constructFromData( $lang );
|
|
}
|
|
|
|
static::$outputSalt = mt_rand();
|
|
|
|
static::$gotAll = TRUE;
|
|
}
|
|
return static::$multitons;
|
|
}
|
|
|
|
/**
|
|
* Get the enabled languages
|
|
*
|
|
* @param null|\IPS\Db\Select $iterator Select iterator
|
|
* @return array
|
|
*/
|
|
public static function getEnabledLanguages( $iterator=NULL )
|
|
{
|
|
$languages = static::languages($iterator);
|
|
$enabledLanguages = array();
|
|
foreach ( $languages AS $id => $lang )
|
|
{
|
|
if ( $lang->enabled )
|
|
{
|
|
$enabledLanguages[$id] = $lang;
|
|
}
|
|
}
|
|
|
|
return $enabledLanguages;
|
|
}
|
|
|
|
/**
|
|
* Get default language ID
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function defaultLanguage()
|
|
{
|
|
if ( !static::$gotAll )
|
|
{
|
|
static::languages();
|
|
}
|
|
return static::$defaultLanguageId;
|
|
}
|
|
|
|
/**
|
|
* Get language object for installer
|
|
*
|
|
* @return static
|
|
*/
|
|
public static function setupLanguage()
|
|
{
|
|
$obj = new \IPS\Lang\Setup\Lang;
|
|
require \IPS\ROOT_PATH . '/' . \IPS\CP_DIRECTORY . '/install/lang.php';
|
|
$obj->words = $lang;
|
|
$obj->set_short( ( mb_strtoupper( mb_substr( PHP_OS, 0, 3 ) ) === 'WIN' ) ? 'english' : 'en_US' );
|
|
$obj->wordsLoaded = TRUE;
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* Add upgrader language bits
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function upgraderLanguage()
|
|
{
|
|
$obj = new \IPS\Lang\Upgrade\Lang;
|
|
require \IPS\ROOT_PATH . '/' . \IPS\CP_DIRECTORY . '/upgrade/lang.php';
|
|
$obj->words = $lang;
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* Auto detect language
|
|
*
|
|
* @param string $httpAcceptLanguage HTTP Accept-Language header
|
|
* @return int|NULL ID Of preferred language or NULL if could not be autodetected
|
|
*/
|
|
public static function autoDetectLanguage( $httpAcceptLanguage )
|
|
{
|
|
$preferredLanguage = NULL;
|
|
|
|
if( mb_strpos( $httpAcceptLanguage, ',' ) )
|
|
{
|
|
$httpAcceptLanguage = explode( ',', $httpAcceptLanguage );
|
|
$httpAcceptLanguage = $httpAcceptLanguage[0];
|
|
}
|
|
$httpAcceptLanguage = explode( '-', mb_strtolower( $httpAcceptLanguage ) );
|
|
|
|
foreach ( static::languages() as $lang )
|
|
{
|
|
if( !$lang->enabled )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( preg_match( '/^\w{2}[-_]\w{2}($|\.)/i', $lang->short ) ) // This will only work for Unix-style locales
|
|
{
|
|
$langCode = \strtolower( \substr( $lang->short, 0, 2 ) );
|
|
$countryCode = \strtolower( \substr( $lang->short, -2 ) );
|
|
|
|
if ( $langCode === $httpAcceptLanguage[0] )
|
|
{
|
|
$preferredLanguage = $lang->id;
|
|
|
|
/* Some browsers are silly and send HTTP_ACCEPT_LANGUAGE like this: en,en-US;q=0.9 */
|
|
/* I'm looking at you, Opera */
|
|
if ( isset( $httpAcceptLanguage[1] ) )
|
|
{
|
|
if ( $countryCode === $httpAcceptLanguage[1] )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $preferredLanguage;
|
|
}
|
|
|
|
/**
|
|
* Save translatable language strings
|
|
*
|
|
* @param string|int|\IPS\Application|\IPS\Plugin $appOrPlugin Application key or Plugin ID
|
|
* @param string $key Word key
|
|
* @param string|array $values The values
|
|
* @param bool $js Expose to JavaScript?
|
|
* @param string|NULL $default If the administrator added this word, this is the default value.
|
|
* @return void
|
|
*/
|
|
public static function saveCustom( $appOrPlugin, $key, $values, $js=FALSE, $default=NULL )
|
|
{
|
|
/* Values is a string, so use this value for all languages */
|
|
$isCustom = FALSE;
|
|
if ( $default !== NULL )
|
|
{
|
|
$isCustom = TRUE;
|
|
}
|
|
|
|
if ( !\is_array( $values ) )
|
|
{
|
|
if ( $default === NULL )
|
|
{
|
|
$default = $values;
|
|
}
|
|
|
|
$values = array();
|
|
|
|
foreach ( static::languages() as $lang )
|
|
{
|
|
$values[ $lang->id ] = $default;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( \count( $values ) == 0 )
|
|
{
|
|
return;
|
|
}
|
|
foreach ( static::languages() as $lang )
|
|
{
|
|
if ( !isset( $values[ $lang->id ] ) )
|
|
{
|
|
$values[ $lang->id ] = '';
|
|
}
|
|
else if ( $lang->default AND $default === NULL )
|
|
{
|
|
$default = $values[ $lang->id ];
|
|
}
|
|
}
|
|
}
|
|
|
|
$currentValues = iterator_to_array( \IPS\Db::i()->select( '*', 'core_sys_lang_words', array( 'word_key=?', $key ) )->setKeyField('lang_id') );
|
|
foreach ( $values as $langId => $value )
|
|
{
|
|
if ( isset( $currentValues[ $langId ] ) )
|
|
{
|
|
\IPS\Db::i()->update( 'core_sys_lang_words', array( 'word_default' => $default, 'word_custom' => $value ), array( 'lang_id=? AND word_key=?', $langId, $key ) );
|
|
}
|
|
else
|
|
{
|
|
if ( $appOrPlugin instanceof \IPS\Application )
|
|
{
|
|
$appOrPlugin = $appOrPlugin->directory;
|
|
}
|
|
elseif ( $appOrPlugin instanceof \IPS\Plugin )
|
|
{
|
|
$appOrPlugin = $appOrPlugin->_id;
|
|
}
|
|
|
|
$insert = array(
|
|
'lang_id' => $langId,
|
|
'word_app' => ( \is_string( $appOrPlugin ) ) ? $appOrPlugin : NULL,
|
|
'word_plugin' => ( \is_numeric( $appOrPlugin ) ) ? $appOrPlugin : NULL,
|
|
'word_key' => $key,
|
|
'word_default' => $default,
|
|
'word_custom' => $value,
|
|
'word_js' => $js,
|
|
'word_export' => FALSE,
|
|
'word_is_custom'=> ( $isCustom === TRUE )
|
|
);
|
|
|
|
try
|
|
{
|
|
\IPS\Db::i()->replace( 'core_sys_lang_words', $insert );
|
|
}
|
|
catch( \IPS\Db\Exception $e )
|
|
{
|
|
/* The upgrader can try to insert custom language strings before the word_is_custom column is added in 4.5 */
|
|
if( $e->getCode() == 1054 )
|
|
{
|
|
unset( $insert['word_is_custom'] );
|
|
|
|
\IPS\Db::i()->replace( 'core_sys_lang_words', $insert );
|
|
}
|
|
else
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isset( static::$multitons[ $langId ] ) )
|
|
{
|
|
static::$multitons[ $langId ]->words[ $key ] = $value;
|
|
}
|
|
|
|
if ( $js )
|
|
{
|
|
\IPS\Output::clearJsFiles( 'global', 'root', 'js_lang_' . $langId . '.js' );
|
|
}
|
|
|
|
if ( $key === '_list_format_' )
|
|
{
|
|
unset( \IPS\Data\Store::i()->listFormats );
|
|
}
|
|
|
|
if ( \substr( $key, 0, 10 ) === 'num_short_' )
|
|
{
|
|
unset( \IPS\Data\Store::i()->shortFormats );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy custom values to a different key
|
|
*
|
|
* @param string $app Application Key
|
|
* @param string $key Word key
|
|
* @param string $newKey New Word Key
|
|
* @param string $newApp New Application Key, if different
|
|
* @return void
|
|
*/
|
|
public static function copyCustom( $app, $key, $newKey, $newApp=NULL )
|
|
{
|
|
$values = array();
|
|
foreach ( \IPS\Db::i()->select( 'lang_id, word_default, word_custom', 'core_sys_lang_words', array( 'word_app=? AND word_key=?', $app, $key ) )->setKeyField('lang_id') as $langId => $data )
|
|
{
|
|
$values[ $langId ] = $data['word_custom'] ?: $data['word_default'];
|
|
}
|
|
|
|
foreach( $values as $row )
|
|
{
|
|
static::saveCustom( $newApp ?: $app, $newKey, $values );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete translatable language strings
|
|
*
|
|
* @param string $app Application key
|
|
* @param string $key Word key
|
|
* @return void
|
|
*/
|
|
public static function deleteCustom( $app, $key )
|
|
{
|
|
\IPS\Db::i()->delete( 'core_sys_lang_words', array( 'word_app=? AND word_key=?', $app, $key ) );
|
|
}
|
|
|
|
/**
|
|
* Validate a locale
|
|
*
|
|
* @param string $locale The locale to test
|
|
* @return void
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
public static function validateLocale( $locale )
|
|
{
|
|
if ( $locale != 'x' )
|
|
{
|
|
$success = FALSE;
|
|
$currentLocale = setlocale( LC_ALL, '0' );
|
|
foreach ( array( "{$locale}.UTF-8", "{$locale}.UTF8", $locale ) as $l )
|
|
{
|
|
$test = setlocale( LC_ALL, $l );
|
|
|
|
if ( $test !== FALSE )
|
|
{
|
|
$success = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static::restoreLocale( $currentLocale );
|
|
|
|
if ( $success === FALSE )
|
|
{
|
|
throw new \InvalidArgumentException( 'lang_short_err' );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Import IN_DEV languages to the database
|
|
*
|
|
* @param string $app Application directory
|
|
* @return void
|
|
*/
|
|
public static function importInDev( $app )
|
|
{
|
|
/* Import the language files */
|
|
$lang = array();
|
|
|
|
/* Get all installed languages */
|
|
$languages = array_keys( \IPS\Lang::languages() );
|
|
$version = \IPS\Application::load( $app )->long_version;
|
|
|
|
require \IPS\ROOT_PATH . "/applications/{$app}/dev/lang.php";
|
|
|
|
\IPS\Db::i()->delete( 'core_sys_lang_words', array( 'word_app=? AND word_export=1', $app ) );
|
|
|
|
foreach ( $lang as $k => $v )
|
|
{
|
|
$inserts = array();
|
|
foreach( $languages as $languageId )
|
|
{
|
|
$inserts[] = array(
|
|
'word_app' => $app,
|
|
'word_key' => $k,
|
|
'lang_id' => $languageId,
|
|
'word_default' => $v,
|
|
'word_custom' => NULL,
|
|
'word_default_version' => $version,
|
|
'word_custom_version' => NULL,
|
|
'word_js' => 0,
|
|
'word_export' => 1,
|
|
);
|
|
}
|
|
|
|
\IPS\Db::i()->replace( 'core_sys_lang_words', $inserts );
|
|
}
|
|
|
|
$lang = array();
|
|
|
|
require \IPS\ROOT_PATH . "/applications/{$app}/dev/jslang.php";
|
|
foreach ( $lang as $k => $v )
|
|
{
|
|
$inserts = array();
|
|
foreach( $languages as $languageId )
|
|
{
|
|
$inserts[] = array(
|
|
'word_app' => $app,
|
|
'word_key' => $k,
|
|
'lang_id' => $languageId,
|
|
'word_default' => $v,
|
|
'word_custom' => NULL,
|
|
'word_default_version' => $version,
|
|
'word_custom_version' => NULL,
|
|
'word_js' => 1,
|
|
'word_export' => 1,
|
|
);
|
|
}
|
|
|
|
\IPS\Db::i()->replace( 'core_sys_lang_words', $inserts );
|
|
}
|
|
}
|
|
|
|
/* !Lang - Instance */
|
|
|
|
/**
|
|
* @brief Locale data
|
|
*/
|
|
public $locale = array();
|
|
|
|
/**
|
|
* @brief Codepage used, if Windows
|
|
*/
|
|
public $codepage = NULL;
|
|
|
|
/**
|
|
* Set the appropriate locale
|
|
*
|
|
* @return void
|
|
* @note <a href='https://bugs.php.net/bug.php?id=18556'>Turkish and some other locales do not work properly</a>
|
|
*/
|
|
public function setLocale()
|
|
{
|
|
$result = setlocale( LC_ALL, $this->short );
|
|
|
|
/* Some locales in some PHP versions break things drastically */
|
|
if( \in_array( 'ips\\db\\_select', get_declared_classes() ) AND !\in_array( 'IPS\\Db\\_Select', get_declared_classes() ) )
|
|
{
|
|
setlocale( LC_CTYPE, 'en_US.UTF-8' );
|
|
}
|
|
|
|
/* If this is Windows, store the codepage as we will need it again later */
|
|
if( mb_strtoupper( mb_substr( PHP_OS, 0, 3 ) ) === 'WIN' )
|
|
{
|
|
$codepage = preg_replace( "/^(.+?)\.(.+?)$/i", "$2", $result );
|
|
|
|
if( $codepage !== $result )
|
|
{
|
|
$this->codepage = $codepage;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore a previous locale
|
|
*
|
|
* @param string $previousLocale Value from setlocale( LC_ALL, '0' )
|
|
* @return void
|
|
*/
|
|
public static function restoreLocale( $previousLocale )
|
|
{
|
|
foreach( explode( ";", $previousLocale ) 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 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Cached preferred date format
|
|
*/
|
|
protected $preferredDateFormat = NULL;
|
|
|
|
/**
|
|
* Get the preferred date format for this locale
|
|
*
|
|
* @return string
|
|
*/
|
|
public function preferredDateFormat()
|
|
{
|
|
if( $this->preferredDateFormat === NULL )
|
|
{
|
|
/* Make sure the locale has been set, important for things like the js date_format variable */
|
|
$this->setLocale();
|
|
|
|
$date = new \IPS\DateTime('1992-03-04');
|
|
$this->preferredDateFormat = str_replace( array( '1992', '92', '03', '3', $date->strFormat('%B'), $date->strFormat('%b'), '04', ' 4', '4' ), array( 'YY', 'YY', 'MM', 'MM', 'MM', 'MM', 'DD', 'DD', 'DD' ), $date->localeDate() );
|
|
}
|
|
|
|
return $this->preferredDateFormat;
|
|
}
|
|
|
|
/**
|
|
* Convert the character set for locale-aware strings on Windows systems
|
|
*
|
|
* @param string $text Text to convert
|
|
* @return string
|
|
*/
|
|
public function convertString( $text )
|
|
{
|
|
/* We only do this on Windows */
|
|
if( mb_strtoupper( mb_substr( PHP_OS, 0, 3 ) ) !== 'WIN' )
|
|
{
|
|
return $text;
|
|
}
|
|
|
|
/* And only if iconv() exists */
|
|
if( !\function_exists( 'iconv' ) )
|
|
{
|
|
return $text;
|
|
}
|
|
|
|
/* And only if we have a codepage stored */
|
|
if( !$this->codepage )
|
|
{
|
|
return $text;
|
|
}
|
|
|
|
/* Convert the codepage to UTF-8 (if it is valid) and return */
|
|
try
|
|
{
|
|
if( ( $converted = iconv( "CP" . $this->codepage, "UTF-8", $text ) ) !== FALSE )
|
|
{
|
|
return $converted;
|
|
}
|
|
else
|
|
{
|
|
throw new \ErrorException;
|
|
}
|
|
}
|
|
catch( \ErrorException $e )
|
|
{
|
|
return $text;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Words
|
|
*/
|
|
public $words = array();
|
|
|
|
/**
|
|
* @brief Original Words
|
|
*/
|
|
public $originalWords = array();
|
|
|
|
/**
|
|
* Check Keys Exist
|
|
*
|
|
* @param string $key Language key
|
|
* @return bool
|
|
*/
|
|
public function checkKeyExists( $key )
|
|
{
|
|
if( isset( $this->words[ $key ] ) )
|
|
{
|
|
return TRUE;
|
|
}
|
|
else if ( array_key_exists( $key, $this->words ) and $this->words[ $key ] === NULL )
|
|
{
|
|
/* Language key has been preloaded but does not exist */
|
|
return FALSE;
|
|
}
|
|
else if( $this->wordsLoaded or \IPS\IN_DEV )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
try
|
|
{
|
|
$lang = \IPS\Db::i()->select( 'word_key, word_default, word_custom', 'core_sys_lang_words', array( 'lang_id=? AND word_key=?', \IPS\Member::loggedIn()->language()->id, $key ) )->first();
|
|
|
|
$value = $lang['word_custom'] ?: $lang['word_default'];
|
|
|
|
$this->words[ $key ] = $value;
|
|
|
|
return TRUE;
|
|
}
|
|
catch ( \UnderflowException $e )
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Language String
|
|
*
|
|
* @param string|array $key Language key or array of keys
|
|
* @return string|array Language string or array of key => string pairs
|
|
*/
|
|
public function get( $key )
|
|
{
|
|
$return = array();
|
|
$keysToLoad = array();
|
|
|
|
if ( \is_array( $key ) )
|
|
{
|
|
foreach( $key as $k )
|
|
{
|
|
if ( \in_array( $k, array_keys( $this->words ), true ) )
|
|
{
|
|
$return[ $k ] = $this->words[ $k ];
|
|
}
|
|
else
|
|
{
|
|
$keysToLoad[] = "'" . \IPS\Db::i()->real_escape_string( $k ) . "'";
|
|
}
|
|
}
|
|
|
|
if ( !\count( $keysToLoad ) )
|
|
{
|
|
return $return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( isset( $this->words[ $key ] ) )
|
|
{
|
|
return $this->words[ $key ];
|
|
}
|
|
|
|
$keysToLoad = array( "'" . \IPS\Db::i()->real_escape_string( $key ) . "'" );
|
|
}
|
|
|
|
foreach( \IPS\Db::i()->select( 'word_key, word_default, word_custom', 'core_sys_lang_words', array( "lang_id=? AND word_key IN(" . implode( ",", $keysToLoad ) . ")", $this->id ) ) as $lang )
|
|
{
|
|
$value = $lang['word_custom'] ?: $lang['word_default'];
|
|
|
|
$this->words[ $lang['word_key'] ] = $value;
|
|
$return[ $lang['word_key' ] ] = $value;
|
|
}
|
|
|
|
/* If we're using an array, fill any missings strings with NULL to prevent duplicate queries */
|
|
if ( \is_array( $key ) )
|
|
{
|
|
foreach( $key as $k )
|
|
{
|
|
if ( !\in_array( $k, $return ) and ! array_key_exists( $k, $this->words ) )
|
|
{
|
|
$return[ $k ] = NULL;
|
|
$this->words[ $k ] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !\count( $return ) )
|
|
{
|
|
throw new \UnderflowException( ( \is_string( $key ) ? 'lang_not_exists__' . $key : 'lang_not_exists__' . implode( ',', $key ) ) );
|
|
}
|
|
|
|
return \is_string( $key ) ? $this->words[ $key ] : $return;
|
|
}
|
|
|
|
/**
|
|
* Add to output stack
|
|
*
|
|
* @param string $key Language key
|
|
* @param bool|null $vle Add VLE tags?
|
|
* @param array $options Options
|
|
* @return string Unique id
|
|
*/
|
|
public function addToStack( $key, $vle=TRUE, $options=array() )
|
|
{
|
|
/* Setup? */
|
|
if( $this->wordsLoaded === TRUE )
|
|
{
|
|
return ( isset( $this->words[ $key ] ) ) ? $this->words[ $key ] : $key;
|
|
}
|
|
|
|
/* Get it */
|
|
if( isset( $this->outputStack[ $key ] ) )
|
|
{
|
|
return htmlspecialchars( $key, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
|
|
}
|
|
|
|
$id = md5( 'ipslang_' . static::$outputSalt . $key . json_encode( $options ) );
|
|
$this->outputStack[ $id ]['key'] = $key;
|
|
$this->outputStack[ $id ]['options'] = $options;
|
|
$this->outputStack[ $id ]['vle'] = $vle;
|
|
|
|
/* Return */
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* Pluralize
|
|
*
|
|
* @param string $string Language string to pluralize
|
|
* @param array $params Parameters to pluraizlie with
|
|
* @param string $formatter Number formatter (default is formatNumber)
|
|
* @return string
|
|
* @note You can use the following wildcards to do special things
|
|
* @li ? is a fallback, so anything not matched will use it
|
|
* @li * is a beginning wildcard, so anything that ENDS with the number supplied will match
|
|
* @li % is an ending wildcard, so anything that BEGINS with the number supplied will match
|
|
* @li # (optional) will be replaced with the actual value
|
|
* @code
|
|
{# [1:test][*2:tests][%3:no tests][?:finals]} will result in 1 test, 2 tests, 12 tests, 3 no tests, 35 no tests, 8 finals
|
|
{!1#[1:January][2:February][3:March][4:April][5:May][6:June][7:July][8:August][9:September][10:October][11:November][12:December]} {0#}
|
|
{!#[?:%s liked]} %s
|
|
* @endcode
|
|
*/
|
|
public function pluralize( $string, $params, $formatter=NULL )
|
|
{
|
|
$i = 0;
|
|
$openCurly = '--' . mt_rand() . '--';
|
|
$closeCurly = '--' . mt_rand() . '--';
|
|
|
|
/* Prevent nested { } breaking the syntax */
|
|
$string = preg_replace_callback( '/(\{!(?:.+?)?(?:\d+?)?\#\[)(.*?)(\]\})/', function( $matches ) use ( $openCurly, $closeCurly )
|
|
{
|
|
$replaced = str_replace( '{', $openCurly, $matches[2] );
|
|
$replaced = str_replace( '}', $closeCurly, $replaced );
|
|
return $matches[1] . $replaced . $matches[3];
|
|
}, $string );
|
|
|
|
$numberFormatter = array( $this, 'formatNumber' );
|
|
return preg_replace_callback( '/\{!?(\d+?)?#(.*?)\}/', function( $format ) use ( $params, $i, $numberFormatter, $openCurly, $closeCurly, $formatter )
|
|
{
|
|
$originalNumber = $format[1];
|
|
if ( !$format[1] or $format[1] == '!' )
|
|
{
|
|
$format[1] = $i;
|
|
$i++;
|
|
}
|
|
|
|
/* Format now so that 0 is really 0 and not '' or null */
|
|
$preFormattedNumber = $params[ $format[1] ]; // We need 7300 later, not 7,3000
|
|
$params[ $format[1] ] = $numberFormatter( $params[ $format[1] ] );
|
|
$fallback = NULL;
|
|
$value = NULL;
|
|
$token = '--' . mt_rand() . '--';
|
|
|
|
/* We want to ensure that manually escaped # are not switched */
|
|
$format[2] = str_replace( '\#', $token, $format[2] );
|
|
|
|
/* This regex is tricky: It matches [ followed by anything NOT : until :, then any character, then everything that is not a [ until ] */
|
|
preg_match_all( '/\[([^:]+):(.[^\[]*)\]/', $format[2], $matches );
|
|
|
|
foreach ( $matches[1] as $k => $v )
|
|
{
|
|
if ( $v == '?' )
|
|
{
|
|
$fallback = preg_replace( '/(?!&)#(?!\d{2,4};)/', $params[ $format[1] ], $matches[2][ $k ] );
|
|
}
|
|
elseif( ( mb_substr( $v, 0, 1 ) === '%' and ( mb_substr( $v, 1 ) == $params[ $format[1] ] ) ) )
|
|
{
|
|
$value = preg_replace( '/(?!&)#(?!\d{2,4};)/', $params[ $format[1] ], $matches[2][ $k ] );
|
|
// We don't break in case there is a better match
|
|
}
|
|
elseif( ( mb_substr( $v, 0, 1 ) === '*' and ( mb_substr( $v, -( mb_strlen( mb_substr( $v, 1 ) ) ) ) == mb_substr( $params[ $format[1] ], -( mb_strlen( mb_substr( $v, 1 ) ) ) ) ) ) )
|
|
{
|
|
$value = preg_replace( '/(?!&)#(?!\d{2,4};)/', $params[ $format[1] ], $matches[2][ $k ] );
|
|
// We don't break in case there is a better match
|
|
}
|
|
elseif ( ( $v === $params[ $format[1] ] ) )
|
|
{
|
|
$value = preg_replace( '/(?!&)#(?!\d{2,4};)/', $params[ $format[1] ], $matches[2][ $k ] );
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$finalFormattedNumber = ( $formatter == 'short' ) ? $this->formatNumberShort( $preFormattedNumber ) : $params[ $format[1] ];
|
|
$return = rtrim( ltrim( $format[0], '{' ), '}' );
|
|
$return = str_replace( "!{$originalNumber}#", '', $return );
|
|
$return = str_replace( array( "{$format[1]}#", '#' ), $finalFormattedNumber, $return );
|
|
$return = preg_replace( '/\[.+\]/', ( $value === NULL ? $fallback : $value ), $return );
|
|
|
|
$return = str_replace( $token, '#', $return );
|
|
$return = str_replace( $openCurly, '{', $return );
|
|
return str_replace( $closeCurly, '}', $return );
|
|
}, $string );
|
|
}
|
|
|
|
/**
|
|
* Format Number
|
|
*
|
|
* @param number $number The number to format
|
|
* @param int $decimals Number of decimal places
|
|
* @return string
|
|
*/
|
|
public function formatNumber( $number, $decimals=0 )
|
|
{
|
|
return number_format( \floatval( $number ), \floatval( $decimals ), $this->locale['decimal_point'], $this->locale['thousands_sep'] );
|
|
}
|
|
|
|
/**
|
|
* Format a number in a short format (e.g. 1.6k, instead of 1,558)
|
|
*
|
|
* @param number $number The number to format
|
|
* @param int $postDecimal Number of numbers to show after the decimal (1.1k, 1.12k, etc)
|
|
* @return string
|
|
*/
|
|
public function formatNumberShort( $number, $postDecimal=1 )
|
|
{
|
|
$lang = NULL;
|
|
$origNumber = $number;
|
|
|
|
if ( \IPS\IN_DEV )
|
|
{
|
|
foreach( array( 'num_short_billion', 'num_short_million', 'num_short_thousand' ) as $word )
|
|
{
|
|
$format[ $word ] = $this->words[ $word ];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !isset( \IPS\Data\Store::i()->shortFormats ) )
|
|
{
|
|
$formats = array();
|
|
foreach ( \IPS\Db::i()->select( array( 'lang_id', 'word_key', 'word_custom', 'word_default' ), 'core_sys_lang_words', array( \IPS\Db::i()->in('word_key', array( 'num_short_billion', 'num_short_million', 'num_short_thousand' ) ) ) ) as $row )
|
|
{
|
|
$formats[ $row['lang_id'] ][ $row['word_key'] ] = $row['word_custom'] ?: $row['word_default'];
|
|
}
|
|
\IPS\Data\Store::i()->shortFormats = $formats;
|
|
}
|
|
|
|
$format = \IPS\Data\Store::i()->shortFormats[ $this->id ];
|
|
}
|
|
|
|
/* Hundreds */
|
|
if ( $number >= 1000 )
|
|
{
|
|
$number = round( $number, -3 + $postDecimal );
|
|
}
|
|
|
|
/* 1 billion or more */
|
|
if ( $number >= 1000000000 )
|
|
{
|
|
$number = number_format( $number / 1000000000, $postDecimal, $this->locale['decimal_point'], $this->locale['thousands_sep'] );
|
|
$lang = 'num_short_billion';
|
|
}
|
|
|
|
/* all the millions */
|
|
else if ( $number >= 1000000 )
|
|
{
|
|
$number = number_format( $number / 1000000, $postDecimal, $this->locale['decimal_point'], $this->locale['thousands_sep'] );
|
|
$lang = 'num_short_million';
|
|
}
|
|
|
|
/* 1,000 to 999,000 */
|
|
else if ( $number >= 1000 )
|
|
{
|
|
$number = number_format( $number / 1000, $postDecimal, $this->locale['decimal_point'], $this->locale['thousands_sep'] );
|
|
$lang = 'num_short_thousand';
|
|
}
|
|
|
|
// Ensure we don't return 1.0, but rather 1
|
|
if ( \substr( $number, -2 ) === $this->locale['decimal_point'] . "0" )
|
|
{
|
|
$number = \substr($number, 0, -2);
|
|
}
|
|
|
|
if ( $lang )
|
|
{
|
|
return sprintf( $format[ $lang ], $number );
|
|
}
|
|
|
|
return $origNumber;
|
|
}
|
|
|
|
/**
|
|
* Format List
|
|
* Takes an array and returns a string, appropriate for the language (e.g. "a, b and c")
|
|
*
|
|
* Relies on the _list_format_ language string which should be an example list of three items using the keys a, b and c.
|
|
* Any can be capitalised to run ucfirst on that item
|
|
*
|
|
* Examples if $items = array( 'foo', 'bar', 'baz', 'moo' );
|
|
* If _list_format_ is this: Output will be this:
|
|
* a, b and c foo, bar, baz and moo
|
|
* A, B und C Foo, Bar, Baz und Moo
|
|
* a; b; c. foo; bar; baz; moo.
|
|
*
|
|
* @param array $items The items for the list
|
|
* @param string $format If provided, will override _list_format_
|
|
* @return string
|
|
*/
|
|
public function formatList( $items, $format=NULL )
|
|
{
|
|
$items = array_values( $items );
|
|
|
|
if ( $format === NULL )
|
|
{
|
|
if ( \IPS\IN_DEV )
|
|
{
|
|
$format = $this->words['_list_format_'];
|
|
}
|
|
else
|
|
{
|
|
if ( !isset( \IPS\Data\Store::i()->listFormats ) )
|
|
{
|
|
$formats = array();
|
|
foreach ( \IPS\Db::i()->select( array( 'lang_id', 'word_custom', 'word_default' ), 'core_sys_lang_words', array( 'word_key=?', '_list_format_' ) ) as $row )
|
|
{
|
|
$formats[ $row['lang_id'] ] = $row['word_custom'] ?: $row['word_default'];
|
|
}
|
|
\IPS\Data\Store::i()->listFormats = $formats;
|
|
}
|
|
$format = \IPS\Data\Store::i()->listFormats[ $this->id ];
|
|
}
|
|
}
|
|
|
|
preg_match( '/(^|^(.+?)\s)\b(a)\b(.+?\s?)\b(b)\b(.+?\s?)\b(c)\b(.+?)?$/i', $format, $matches );
|
|
|
|
$return = $matches[1];
|
|
for ( $i = 0; $i<\count( $items ); $i++ )
|
|
{
|
|
/* Pluralize can be used after, so avoid triggering the '#', '#[...]' syntax there */
|
|
$items[ $i ] = preg_replace( '/#(\s|\[)/', '\#\1', $items[ $i ] );
|
|
$upper = FALSE;
|
|
if ( $i == 0 )
|
|
{
|
|
$upper = ( $matches[3] === 'A' );
|
|
}
|
|
elseif ( $i == \count( $items ) - 1 )
|
|
{
|
|
$upper = ( $matches[7] === 'C' );
|
|
}
|
|
else
|
|
{
|
|
$upper = ( $matches[5] === 'B' );
|
|
}
|
|
|
|
$return .= ( $upper ? ucfirst( $items[ $i ] ) : $items[ $i ] );
|
|
|
|
if ( $i == \count( $items ) - 2 )
|
|
{
|
|
$return .= $matches[6];
|
|
}
|
|
elseif ( $i != \count( $items ) - 1 )
|
|
{
|
|
$return .= $matches[4];
|
|
}
|
|
}
|
|
if ( isset( $matches[8] ) )
|
|
{
|
|
$return .= $matches[8];
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Search translatable language strings
|
|
*
|
|
* @param string $prefix Prefix used
|
|
* @param string $query Search query
|
|
* @param bool $alsoSearchDefault If TRUE, will also search the default value
|
|
* @return array
|
|
*/
|
|
public function searchCustom( $prefix, $query, $alsoSearchDefault=FALSE )
|
|
{
|
|
$return = array();
|
|
|
|
$where = array();
|
|
$where[] = array( "lang_id=?", $this->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 <a href="https://tools.ietf.org/html/bcp47">BCP 47 - Tags for Identifying Languages</a>
|
|
*/
|
|
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
|
|
* @param string $current The current locale
|
|
* @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' );
|
|
$localesToTest = ( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) !== 'win' ) ? array( "{$values['lang_short']}.UTF-8", "{$values['lang_short']}.UTF8" ) : array( $values['lang_short'] );
|
|
|
|
foreach ( $localesToTest 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 ) );
|
|
}
|
|
|
|
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()}";
|
|
}
|
|
|
|
/**
|
|
* [Node] Return the custom badge for each row
|
|
*
|
|
* @return NULL|array Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
|
|
*/
|
|
protected function get__badge()
|
|
{
|
|
/* Is there an update to show? */
|
|
$badge = NULL;
|
|
|
|
if ( $this->update_data )
|
|
{
|
|
$data = json_decode( $this->update_data, TRUE );
|
|
|
|
if( !empty( $data['longversion'] ) AND $data['longversion'] > $this->version_long )
|
|
{
|
|
$released = NULL;
|
|
|
|
if( $data['released'] AND \intval( $data['released'] ) == $data['released'] AND \strlen( $data['released'] ) == 10 )
|
|
{
|
|
$released = (string) \IPS\DateTime::ts( $data['released'] )->localeDate();
|
|
}
|
|
else if( $data['released'] )
|
|
{
|
|
$released = $data['released'];
|
|
}
|
|
|
|
$badge = array(
|
|
0 => 'new',
|
|
1 => '',
|
|
2 => \IPS\Theme::i()->getTemplate( 'global', 'core' )->updatebadge( $data['version'], $data['updateurl'], $released )
|
|
);
|
|
}
|
|
}
|
|
|
|
return $badge;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get Description
|
|
*
|
|
* @return string|null
|
|
*/
|
|
protected function get__description()
|
|
{
|
|
return \IPS\Theme::i()->getTemplate( 'customization', 'core' )->langDescription( $this );
|
|
}
|
|
|
|
/**
|
|
* 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' ) AND !$this->marketplace_id )
|
|
{
|
|
$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}" ),
|
|
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => $this->_title )
|
|
);
|
|
}
|
|
|
|
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}" )->csrf(),
|
|
);
|
|
}
|
|
|
|
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' );
|
|
$result = 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' );
|
|
}
|
|
|
|
/* If this is Windows, store the codepage as we will need it again later */
|
|
if( mb_strtoupper( mb_substr( PHP_OS, 0, 3 ) ) === 'WIN' )
|
|
{
|
|
$codepage = preg_replace( "/^(.+?)\.(.+?)$/i", "$2", $result );
|
|
|
|
if( $codepage !== $result )
|
|
{
|
|
$this->codepage = $codepage;
|
|
}
|
|
}
|
|
|
|
$this->locale = localeconv();
|
|
|
|
foreach( $this->locale as $k => $v )
|
|
{
|
|
if( \is_string( $v ) )
|
|
{
|
|
$this->locale[ $k ] = $this->convertString( $v );
|
|
}
|
|
}
|
|
|
|
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 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete Record
|
|
*
|
|
* @return void
|
|
*/
|
|
public function delete()
|
|
{
|
|
parent::delete();
|
|
\IPS\Db::i()->delete( 'core_sys_lang_words', array( 'lang_id=?', $this->id ) );
|
|
}
|
|
|
|
/**
|
|
* 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'] ] = '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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( '#(<td.+?)dir=([\'"]){dir}([\'"])#i', '\1dir=\2' . $dir . '\3', $output );
|
|
|
|
preg_match_all( '#<(body|html)([^>]+?)>#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|array $output The output string
|
|
* @return string|array
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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 ) )
|
|
{
|
|
if( !\is_string( $haystack ) )
|
|
{
|
|
return $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\.(.+?)\.csrf}/",
|
|
function ( $matches )
|
|
{
|
|
return \IPS\Http\Url::internal( $matches[ 1 ] )->csrf();
|
|
},
|
|
$replacement
|
|
);
|
|
$replacement = preg_replace_callback(
|
|
"/{internal\.(.+?)}/",
|
|
function ( $matches )
|
|
{
|
|
return \IPS\Http\Url::internal( $matches[ 1 ] );
|
|
},
|
|
$replacement
|
|
);
|
|
}
|
|
|
|
/* We do pluralize first in case you have something like "{!#[1:%s has][?:%s have]} %s: %s" */
|
|
if ( !empty( $values[ 'options' ][ 'pluralize' ] ) )
|
|
{
|
|
$replacement = $this->pluralize(
|
|
$replacement,
|
|
$values[ 'options' ][ 'pluralize' ],
|
|
isset( $values[ 'options' ]['format'] ) ? $values[ 'options' ]['format'] : NULL
|
|
);
|
|
}
|
|
|
|
$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 | ENT_DISALLOWED, '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 ( \Throwable $e )
|
|
{
|
|
// If there's the wrong number of parameters because the translator's done it wrong, we can just use empty strings for replacements
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$values['options']['escape'] = TRUE; // Prevent accidentally introducing an XSS vulnerability by using raw strings where something expects a language string
|
|
$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' ][ 'seotitle' ] ) )
|
|
{
|
|
$replacement = \IPS\Http\Url\Friendly::seoTitle( $replacement );
|
|
}
|
|
|
|
if ( isset( $values[ 'options' ][ 'json' ] ) )
|
|
{
|
|
if( isset( $values['options']['jsonEscape'] ) )
|
|
{
|
|
$replacement = mb_substr( json_encode( $replacement, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_UNICODE ), 1, -1 );
|
|
}
|
|
else
|
|
{
|
|
$replacement = mb_substr( json_encode( $replacement ), 1, -1 );
|
|
}
|
|
}
|
|
|
|
if ( isset( $values[ 'options' ][ 'striptags' ] ) )
|
|
{
|
|
$replacement = strip_tags( $replacement );
|
|
}
|
|
|
|
if ( isset( $values[ 'options' ][ 'urlencode' ] ) )
|
|
{
|
|
$replacement = urlencode( $replacement );
|
|
}
|
|
|
|
if ( isset( $values[ 'options' ][ 'rawurlencode' ] ) )
|
|
{
|
|
$replacement = rawurlencode( $replacement );
|
|
}
|
|
|
|
if ( isset( $values[ 'options' ][ 'escape' ] ) )
|
|
{
|
|
$replacement = htmlspecialchars( $replacement, ENT_QUOTES | ENT_DISALLOWED, '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 );
|
|
}
|
|
|
|
if ( isset( $values[ 'options' ][ 'removeLazyLoad' ] ) )
|
|
{
|
|
$replacement = \IPS\Text\Parser::removeLazyLoad( $replacement );
|
|
}
|
|
|
|
/* Remove newlines and any whitespace */
|
|
if ( isset( $values[ 'options' ][ 'removeNewlines' ] ) )
|
|
{
|
|
$replacement = trim( preg_replace( "#\n\s+?#", " ", $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;
|
|
}
|
|
|
|
/**
|
|
* Word Form
|
|
*
|
|
* @param \IPS\Helpers\Form|NULL An existing form object or NULL to create new
|
|
* @return void
|
|
*/
|
|
public static function wordForm( ?\IPS\Helpers\Form &$form = NULL )
|
|
{
|
|
if ( $form === NULL )
|
|
{
|
|
$form = new \IPS\Helpers\Form;
|
|
}
|
|
if ( \IPS\Application::appIsEnabled( 'cms' ) )
|
|
{
|
|
$form->addMessage( 'add_word_reason_pages', 'ipsMessage ipsMessage_info' );
|
|
}
|
|
else
|
|
{
|
|
$form->addMessage( 'add_word_reason', 'ipsMessage ipsMessage_info' );
|
|
}
|
|
$form->add( new \IPS\Helpers\Form\Text( 'word_key', NULL, TRUE, array( 'regex' => '/^([A-Z_][A-Z0-9_]+?)$/i', 'placeholder' => 'my_custom_phrase' ), function( $val ) {
|
|
try
|
|
{
|
|
/* This is a generic check - if key exists in *any* language, don't allow. */
|
|
\IPS\Db::i()->select( '*', 'core_sys_lang_words', array( "word_key=?", $val ) )->first();
|
|
|
|
/* Here? No excepton thrown so error */
|
|
throw new \DomainException( 'word_key_exists' );
|
|
}
|
|
catch( \UnderflowException $e ) {}
|
|
} ) );
|
|
$form->add( new \IPS\Helpers\Form\TextArea( 'word_default', NULL, TRUE, array( 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack( 'my_custom_phrase' ) ) ) );
|
|
if ( \count( static::languages() ) > 1 )
|
|
{
|
|
$form->add( new \IPS\Helpers\Form\Translatable( 'word_custom', NULL, FALSE, array( 'textArea' => TRUE ) ) );
|
|
}
|
|
}
|
|
} |