436 lines
13 KiB
PHP
436 lines
13 KiB
PHP
<?php namespace RainLab\Builder\Classes;
|
|
|
|
use ApplicationException;
|
|
use Symfony\Component\Yaml\Dumper as YamlDumper;
|
|
use SystemException;
|
|
use DirectoryIterator;
|
|
use ValidationException;
|
|
use Yaml;
|
|
use Exception;
|
|
use Config;
|
|
use Lang;
|
|
use File;
|
|
|
|
/**
|
|
* Represents and manages plugin localization files.
|
|
*
|
|
* @package rainlab\builder
|
|
* @author Alexey Bobkov, Samuel Georges
|
|
*/
|
|
class LocalizationModel extends BaseModel
|
|
{
|
|
public $strings;
|
|
|
|
public $language;
|
|
|
|
public $originalLanguage;
|
|
|
|
protected static $fillable = [
|
|
'strings',
|
|
'language'
|
|
];
|
|
|
|
protected $validationRules = [
|
|
'language' => ['required', 'regex:/^[a-z0-9\.\-]+$/i']
|
|
];
|
|
|
|
protected $originalStringArray = [];
|
|
|
|
public function load($language)
|
|
{
|
|
$this->language = $language;
|
|
|
|
$this->originalLanguage = $language;
|
|
|
|
$filePath = $this->getFilePath();
|
|
|
|
if (!File::isFile($filePath)) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_cant_load_file'));
|
|
}
|
|
|
|
if (!$this->validateFileContents($filePath)) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_bad_localization_file_contents'));
|
|
}
|
|
|
|
$strings = include($filePath);
|
|
if (!is_array($strings)) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_file_not_array'));
|
|
}
|
|
|
|
$this->originalStringArray = $strings;
|
|
|
|
if (count($strings) > 0) {
|
|
$dumper = new YamlDumper();
|
|
$this->strings = $dumper->dump($strings, 20, 0, false, true);
|
|
}
|
|
else {
|
|
$this->strings = '';
|
|
}
|
|
|
|
$this->exists = true;
|
|
}
|
|
|
|
public static function initModel($pluginCode, $language)
|
|
{
|
|
$model = new self();
|
|
$model->setPluginCode($pluginCode);
|
|
$model->language = $language;
|
|
|
|
return $model;
|
|
}
|
|
|
|
public function save()
|
|
{
|
|
$data = $this->modelToLanguageFile();
|
|
$this->validate();
|
|
|
|
$filePath = File::symbolizePath($this->getFilePath());
|
|
$isNew = $this->isNewModel();
|
|
|
|
if (File::isFile($filePath)) {
|
|
if ($isNew || $this->originalLanguage != $this->language) {
|
|
throw new ValidationException(['fileName' => Lang::get('rainlab.builder::lang.common.error_file_exists', ['path'=>$this->language.'/'.basename($filePath)])]);
|
|
}
|
|
}
|
|
|
|
$fileDirectory = dirname($filePath);
|
|
if (!File::isDirectory($fileDirectory)) {
|
|
if (!File::makeDirectory($fileDirectory, 0777, true, true)) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$fileDirectory]));
|
|
}
|
|
}
|
|
|
|
if (@File::put($filePath, $data) === false) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.save_error', ['name'=>$filePath]));
|
|
}
|
|
|
|
@File::chmod($filePath);
|
|
|
|
if (!$this->isNewModel() && strlen($this->originalLanguage) > 0 && $this->originalLanguage != $this->language) {
|
|
$this->originalFilePath = $this->getFilePath($this->originalLanguage);
|
|
@File::delete($this->originalFilePath);
|
|
}
|
|
|
|
$this->originalLanguage = $this->language;
|
|
$this->exists = true;
|
|
}
|
|
|
|
public function deleteModel()
|
|
{
|
|
if ($this->isNewModel()) {
|
|
throw new ApplicationException('Cannot delete language file which is not saved yet.');
|
|
}
|
|
|
|
$filePath = File::symbolizePath($this->getFilePath());
|
|
if (File::isFile($filePath)) {
|
|
if (!@unlink($filePath)) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_delete_file'));
|
|
}
|
|
}
|
|
}
|
|
|
|
public function initContent()
|
|
{
|
|
$templatePath = '$/rainlab/builder/classes/localizationmodel/templates/lang.php';
|
|
$templatePath = File::symbolizePath($templatePath);
|
|
|
|
$strings = include($templatePath);
|
|
$dumper = new YamlDumper();
|
|
$this->strings = $dumper->dump($strings, 20, 0, false, true);
|
|
}
|
|
|
|
public static function listPluginLanguages($pluginCodeObj)
|
|
{
|
|
$languagesDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/lang';
|
|
|
|
$languagesDirectoryPath = File::symbolizePath($languagesDirectoryPath);
|
|
|
|
if (!File::isDirectory($languagesDirectoryPath)) {
|
|
return [];
|
|
}
|
|
|
|
$result = [];
|
|
foreach (new DirectoryIterator($languagesDirectoryPath) as $fileInfo) {
|
|
if (!$fileInfo->isDir() || $fileInfo->isDot()) {
|
|
continue;
|
|
}
|
|
|
|
$langFilePath = $fileInfo->getPathname().'/lang.php';
|
|
|
|
if (File::isFile($langFilePath)) {
|
|
$result[] = $fileInfo->getFilename();
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function copyStringsFrom($destinationText, $sourceLanguageCode)
|
|
{
|
|
$sourceLanguageModel = new self();
|
|
$sourceLanguageModel->setPluginCodeObj($this->getPluginCodeObj());
|
|
$sourceLanguageModel->load($sourceLanguageCode);
|
|
|
|
$srcArray = $sourceLanguageModel->getOriginalStringsArray();
|
|
|
|
$languageMixer = new LanguageMixer();
|
|
|
|
return $languageMixer->addStringsFromAnotherLanguage($destinationText, $srcArray);
|
|
}
|
|
|
|
public function getOriginalStringsArray()
|
|
{
|
|
return $this->originalStringArray;
|
|
}
|
|
|
|
public function createStringAndSave($stringKey, $stringValue)
|
|
{
|
|
$stringKey = trim($stringKey, '.');
|
|
|
|
if (!strlen($stringKey)) {
|
|
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_is_empty')]);
|
|
}
|
|
|
|
if (!strlen($stringValue)) {
|
|
throw new ValidationException(['value' => Lang::get('rainlab.builder::lang.localization.string_value_is_empty')]);
|
|
}
|
|
|
|
$originalStringArray = $this->getOriginalStringsArray();
|
|
$languagePrefix = strtolower($this->getPluginCodeObj()->toCode()).'::lang.';
|
|
|
|
$existingStrings = self::convertToStringsArray($originalStringArray, $languagePrefix);
|
|
if (array_key_exists($languagePrefix.$stringKey, $existingStrings)) {
|
|
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_exists')]);
|
|
}
|
|
|
|
$existingSections = self::convertToSectionsArray($originalStringArray);
|
|
if (array_key_exists($stringKey.'.', $existingSections)) {
|
|
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_exists')]);
|
|
}
|
|
|
|
$sectionArray = [];
|
|
self::createStringSections($sectionArray, $stringKey, $stringValue) ;
|
|
|
|
$this->checkKeyWritable($stringKey, $existingStrings, $languagePrefix);
|
|
$newStrings = LanguageMixer::arrayMergeRecursive($originalStringArray, $sectionArray);
|
|
|
|
$dumper = new YamlDumper();
|
|
$this->strings = $dumper->dump($newStrings, 20, 0, false, true);
|
|
|
|
$this->save();
|
|
|
|
return $languagePrefix.$stringKey;
|
|
}
|
|
|
|
public static function getDefaultLanguage()
|
|
{
|
|
$language = Config::get('app.locale');
|
|
|
|
if (!$language) {
|
|
throw new ApplicationException('The default language is not defined in the application configuration (app.locale).');
|
|
}
|
|
|
|
return $language;
|
|
}
|
|
|
|
public static function getPluginRegistryData($pluginCode, $subtype)
|
|
{
|
|
$defaultLanguage = self::getDefaultLanguage();
|
|
|
|
$model = new self();
|
|
$model->setPluginCode($pluginCode);
|
|
$model->language = $defaultLanguage;
|
|
|
|
$filePath = $model->getFilePath();
|
|
if (!File::isFile($filePath)) {
|
|
return [];
|
|
}
|
|
|
|
$model->load($defaultLanguage);
|
|
|
|
$array = $model->getOriginalStringsArray();
|
|
$languagePrefix = strtolower($model->getPluginCodeObj()->toCode()).'::lang.';
|
|
|
|
if ($subtype !== 'sections') {
|
|
return self::convertToStringsArray($array, $languagePrefix);
|
|
}
|
|
|
|
return self::convertToSectionsArray($array);
|
|
}
|
|
|
|
public static function languageFileExists($pluginCode, $language)
|
|
{
|
|
$model = new self();
|
|
$model->setPluginCode($pluginCode);
|
|
$model->language = $language;
|
|
|
|
$filePath = $model->getFilePath();
|
|
return File::isFile($filePath);
|
|
}
|
|
|
|
protected static function createStringSections(&$arr, $path, $value) {
|
|
$keys = explode('.', $path);
|
|
|
|
while ($key = array_shift($keys)) {
|
|
$arr = &$arr[$key];
|
|
}
|
|
|
|
$arr = $value;
|
|
}
|
|
|
|
protected static function convertToStringsArray($stringsArray, $prefix, $currentKey = '')
|
|
{
|
|
$result = [];
|
|
|
|
foreach ($stringsArray as $key=>$value) {
|
|
$newKey = strlen($currentKey) ? $currentKey.'.'.$key : $key;
|
|
|
|
if (is_scalar($value)) {
|
|
$result[$prefix.$newKey] = $value;
|
|
}
|
|
else {
|
|
$result = array_merge($result, self::convertToStringsArray($value, $prefix, $newKey));
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected static function convertToSectionsArray($stringsArray, $currentKey = '')
|
|
{
|
|
$result = [];
|
|
|
|
foreach ($stringsArray as $key=>$value) {
|
|
$newKey = strlen($currentKey) ? $currentKey.'.'.$key : $key;
|
|
|
|
if (is_scalar($value)) {
|
|
$result[$currentKey.'.'] = $currentKey.'.';
|
|
}
|
|
else {
|
|
$result = array_merge($result, self::convertToSectionsArray($value, $newKey));
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function validateLanguage($language)
|
|
{
|
|
return preg_match('/^[a-z0-9\.\-]+$/i', $language);
|
|
}
|
|
|
|
protected function getFilePath($language = null)
|
|
{
|
|
if ($language === null) {
|
|
$language = $this->language;
|
|
}
|
|
|
|
$language = trim($language);
|
|
|
|
if (!strlen($language)) {
|
|
throw new SystemException('The form model language is not set.');
|
|
}
|
|
|
|
if (!$this->validateLanguage($language)) {
|
|
throw new SystemException('Invalid language file name: '.$language);
|
|
}
|
|
|
|
$path = $this->getPluginCodeObj()->toPluginDirectoryPath().'/lang/'.$language.'/lang.php';
|
|
return File::symbolizePath($path);
|
|
}
|
|
|
|
protected function modelToLanguageFile()
|
|
{
|
|
$this->strings = trim($this->strings);
|
|
|
|
if (!strlen($this->strings)) {
|
|
return "<?php return [\n];";
|
|
}
|
|
|
|
try {
|
|
$data = $this->getSanitizedPHPStrings(Yaml::parse($this->strings));
|
|
|
|
$phpData = var_export($data, true);
|
|
$phpData = preg_replace('/^(\s+)\),/m', '$1],', $phpData);
|
|
$phpData = preg_replace('/^(\s+)array\s+\(/m', '$1[', $phpData);
|
|
$phpData = preg_replace_callback('/^(\s+)/m', function($matches) {
|
|
return str_repeat($matches[1], 2); // Increase indentation
|
|
}, $phpData);
|
|
$phpData = preg_replace('/\n\s+\[/m', '[', $phpData);
|
|
$phpData = preg_replace('/^array\s\(/', '[', $phpData);
|
|
$phpData = preg_replace('/^\)\Z/m', ']', $phpData);
|
|
|
|
return "<?php return ".$phpData.";";
|
|
}
|
|
catch (Exception $ex) {
|
|
throw new ApplicationException(sprintf('Cannot parse the YAML content: %s', $ex->getMessage()));
|
|
}
|
|
}
|
|
|
|
protected function validateFileContents($path)
|
|
{
|
|
$fileContents = File::get($path);
|
|
|
|
$stream = new PhpSourceStream($fileContents);
|
|
|
|
$invalidTokens = [
|
|
T_CLASS,
|
|
T_FUNCTION,
|
|
T_INCLUDE,
|
|
T_INCLUDE_ONCE,
|
|
T_REQUIRE,
|
|
T_REQUIRE_ONCE,
|
|
T_EVAL,
|
|
T_ECHO,
|
|
T_GOTO,
|
|
T_HALT_COMPILER,
|
|
T_STRING // Unescaped strings - function names, etc.
|
|
];
|
|
|
|
while ($stream->forward()) {
|
|
$tokenCode = $stream->getCurrentCode();
|
|
|
|
if (in_array($tokenCode, $invalidTokens)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function getSanitizedPHPStrings($strings)
|
|
{
|
|
array_walk_recursive($strings, function(&$item, $key){
|
|
if (!is_scalar($item)) {
|
|
return;
|
|
}
|
|
|
|
// In YAML single quotes are escaped with two single quotes
|
|
// http://yaml.org/spec/current.html#id2534365
|
|
$item = str_replace("''", "'", $item);
|
|
});
|
|
|
|
return $strings;
|
|
}
|
|
|
|
protected function checkKeyWritable($stringKey, $existingStrings, $languagePrefix)
|
|
{
|
|
$sectionList = explode('.', $stringKey);
|
|
|
|
$lastElement = array_pop($sectionList);
|
|
while (strlen($lastElement)) {
|
|
if (count($sectionList) > 0) {
|
|
$fullKey = implode('.', $sectionList).'.'.$lastElement;
|
|
}
|
|
else {
|
|
$fullKey = $lastElement;
|
|
}
|
|
|
|
if (array_key_exists($languagePrefix.$fullKey, $existingStrings)) {
|
|
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_is_a_string', ['key'=>$fullKey])]);
|
|
}
|
|
|
|
$lastElement = array_pop($sectionList);
|
|
}
|
|
}
|
|
} |