52 if (empty($path) || !Translate\
IO\Path::isPhpFile($path) || !\preg_match(
"#.+/lang/[a-z0-9]{2}/.+\.php$#", $path))
57 $file = (
new static($path))
58 ->setLangId(Translate\
IO\Path::extractLangId($path));
73 return (
new static($fileIndex->getFullPath()))->setLangId($fileIndex->getLangId());
87 if ($fileIn->getExtension() !==
'php')
92 return (
new static($fileIn->getPath()))->setLangId(Translate\
IO\Path::extractLangId($fileIn->getPath()));
105 if (empty($this->languageId))
107 $this->languageId = Translate\IO\Path::extractLangId($this->
getPath());
110 return $this->languageId;
122 $this->languageId = $languageId;
133 static $encodingCache = [];
134 if (empty($this->sourceEncoding))
137 if (isset($encodingCache[$language]))
139 $this->sourceEncoding = $encodingCache[$language];
143 $this->sourceEncoding = Main\Localization\Translation::getSourceEncoding($language);
144 $encodingCache[$language] = $this->sourceEncoding;
148 return $this->sourceEncoding;
160 $this->sourceEncoding = $encoding;
171 if (empty($this->operatingEncoding))
173 $this->operatingEncoding = Main\Localization\Translation::getCurrentEncoding();
176 return $this->operatingEncoding;
188 $this->operatingEncoding = $encoding;
207 string $content =
'',
208 array $validTokens = [\T_OPEN_TAG, \T_CLOSE_TAG, \T_WHITESPACE, \T_CONSTANT_ENCAPSED_STRING, \T_VARIABLE, \T_COMMENT, \T_DOC_COMMENT],
209 array $validChars = [
'[',
']',
';',
'=']
221 if (empty($content) || !\is_string($content))
223 $this->
addError(
new Main\
Error(
"Parse Error: Empty content"));
227 $tokens = \token_get_all($content);
229 $line = $tokens[0][2] || 1;
230 if (!is_array($tokens[0]) || $tokens[0][0] !== \T_OPEN_TAG)
232 $this->
addError(
new Main\
Error(
"Parse Error: Wrong open tag ".\token_name($tokens[0][0]).
" '{$tokens[0][1]}' at line {$line}"));
237 foreach ($tokens as $token)
239 if (\is_array($token))
243 !\in_array($token[0], $validTokens) ||
244 ($token[0] === \T_VARIABLE && $token[1] !=
'$MESS')
247 $this->
addError(
new Main\
Error(
"Parse Error: Wrong token ". \token_name($token[0]).
" '{$token[1]}' at line {$line}"));
252 elseif (\is_string($token))
254 if (!\in_array($token, $validChars))
257 $this->
addError(
new Main\
Error(
"Parse Error: Expected character '{$token}' at line {$line}"));
280 $this->messages = [];
281 $this->messageCodes = [];
293 $this->
addError(
new Main\
Error(
'Language Id must be filled'));
300 || !\is_string($content)
302 || $content ===
'<?php'
305 $this->
addError(
new Main\
Error(
'Empty content',
'EMPTY_CONTENT'));
312 $convertEncoding = (\mb_strtolower($targetEncoding) != \mb_strtolower(
$sourceEncoding));
313 if ($convertEncoding)
343 foreach (
$messages as $phraseId => $phrase)
345 if ($convertEncoding)
347 $phrase = Main\Text\Encoding::convertEncoding($phrase,
$sourceEncoding, $targetEncoding);
350 $this->messages[$phraseId] = $phrase;
351 $this->messageCodes[] = $phraseId;
366 $this->messages = [];
367 $this->messageCodes = [];
368 $this->messageEnclosure = [];
380 $this->
addError(
new Main\
Error(
'Language Id must be filled'));
387 || !\is_string($content)
389 || $content ===
'<?php'
392 $this->
addError(
new Main\
Error(
'Empty content',
'EMPTY_CONTENT'));
396 $is =
function ($token, $type, $value =
null)
398 if (\is_string($token))
400 return $token === $type;
402 if (\is_array($token))
404 if ($token[0] === $type)
408 return $token[1] === $value;
416 $tokens = \token_get_all($content);
418 $hasPhraseDefinition =
false;
419 foreach ($tokens as $inx => $token)
421 if ($is($token, \T_WHITESPACE))
423 unset($tokens[$inx]);
426 if (!$hasPhraseDefinition && $is($token, \T_VARIABLE,
'$MESS'))
428 $hasPhraseDefinition =
true;
433 if (!$hasPhraseDefinition)
435 $this->
addError(
new Main\
Error(
"There are no phrase definitions"));
439 \array_splice($tokens, 0, 0);
441 $addPhrase =
function ($phraseId, $phraseParts, $isHeredoc =
false)
447 $phraseId = \str_replace(
"\\\\",
"\\", $phraseId);
449 $enclosure = $isHeredoc ?
'<<<' : \mb_substr($phraseParts[0], 0, 1);
454 $part = $phraseParts[0];
460 foreach ($phraseParts as $part)
462 $enclosure = \mb_substr($part, 0, 1);
464 if ($enclosure ===
'"' || $enclosure ===
"'")
474 $this->messages[$phraseId] = $phrase;
475 $this->messageCodes[] = $phraseId;
476 $this->messageEnclosure[$phraseId] = $enclosure;
481 $startPhrase =
false;
490 foreach ($tokens as $inx => &$token)
492 if (!$startPhrase && $is($token, \T_VARIABLE,
'$MESS'))
499 if ($is($token,
'['))
503 elseif ($is($token,
']'))
507 elseif ($is($token,
'='))
511 elseif ($is($token,
';'))
515 elseif ($is($token, \T_CLOSE_TAG))
519 elseif ($is($token, \T_START_HEREDOC))
526 && $is($token, \T_VARIABLE,
'$MESS')
527 && $is($tokens[$inx + 1],
'[')
528 && $is($tokens[$inx + 2], \T_CONSTANT_ENCAPSED_STRING)
531 $clonePhraseId = $tokens[$inx + 2][1];
532 $cloneInx = $whereIsPhrase[$clonePhraseId];
533 $phrase[] = $tokens[$cloneInx][1];
537 if ($is($token, \T_CONSTANT_ENCAPSED_STRING) || $is($token, \T_ENCAPSED_AND_WHITESPACE))
541 $phrase[] = $token[1];
542 $whereIsPhrase[$phraseId] = $inx;
546 $phraseId = $token[1];
552 $addPhrase($phraseId, $phrase, $isHeredoc);
556 $startPhrase =
false;
570 $addPhrase($phraseId, $phrase, $isHeredoc);
587 public function save(): bool
590 $langId = $this->getLangId();
597 $operatingEncoding = $this->getOperatingEncoding();
598 $sourceEncoding = $this->getSourceEncoding();
599 $convertEncoding = (\mb_strtolower($operatingEncoding) != \mb_strtolower($sourceEncoding));
600 if ($convertEncoding)
602 $path = Main\Localization\Translation::convertLangPath($this->getPhysicalPath(), $this->getLangId());
615 foreach ($this->messages as $phraseId => $phrase)
617 if (empty($phrase) && $phrase !==
'0')
622 $phrase = \str_replace([
"\r\n",
"\r"], [
"\n",
''], $phrase);
623 if ($convertEncoding)
625 $phrase = Main\Text\Encoding::convertEncoding($phrase, $operatingEncoding, $sourceEncoding);
628 if (isset($this->messageEnclosure[$phraseId]))
630 $enclosure = $this->messageEnclosure[$phraseId];
633 $phraseId = StringHelper::escapePhp($phraseId,
'"',
"\\\\");
634 if (StringHelper::hasPhpTokens($phraseId,
'"'))
636 $this->
addError(
new Main\
Error(
"Phrase code contains php tokens"));
640 $phrase = StringHelper::escapePhp($phrase, $enclosure);
641 if (StringHelper::hasPhpTokens($phrase, $enclosure))
643 $this->
addError(
new Main\
Error(
"Phrase contains php tokens"));
647 $row =
'$MESS["'. $phraseId.
'"] = ';
648 if ($enclosure ===
'<<<')
650 $row .=
"<<<HTML\n". $phrase.
"\nHTML";
654 $row .= $enclosure. $phrase. $enclosure;
656 $content .=
"\n". $row.
';';
658 unset($phraseId, $phrase, $row);
663 function ($severity, $message, $file, $line)
665 throw new \ErrorException($message, $severity, $severity, $file, $line);
671 $result = parent::putContents(
'<?php'. $content.
"\n");
673 catch (\ErrorException $exception)
675 \restore_error_handler();
676 throw new Main\IO\IoException($exception->getMessage());
679 \restore_error_handler();
681 if ($result ===
false)
684 throw new Main\IO\IoException(
"Couldn't write language file '{$filePath}'");
705 public function removeEmptyParents(): bool
712 if ($parentFolder->isExists() && \count($parentFolder->getChildren()) > 0)
717 if ($parentFolder->isExists())
719 if ($parentFolder->delete() !==
true)
725 if ($parentFolder->getName() ===
'lang')
729 $parentFolder = $parentFolder->getDirectory();
740 public function backup(): bool
755 $langFile = \str_replace(
766 $langFile = \str_replace(
775 $langFile = \str_replace(
782 $backupFolder = Translate\Config::getBackupFolder().
'/'. \dirname($langFile).
'/';
783 if (!Translate\
IO\Path::checkCreatePath($backupFolder))
785 $this->
addError(
new Main\
Error(
"Couldn't create backup path '{$backupFolder}'"));
789 $sourceFilename = \basename($langFile);
790 $prefix = \date(
'YmdHi');
791 $endpointBackupFilename = $prefix.
'_'. $sourceFilename;
792 if (\file_exists($backupFolder. $endpointBackupFilename))
795 while (\file_exists($backupFolder.
'/'. $endpointBackupFilename))
798 $endpointBackupFilename = $prefix.
'_'. $i.
'_'. $sourceFilename;
802 $isSuccessfull = (bool) @\copy($fullPath, $backupFolder.
'/'. $endpointBackupFilename);
803 @\chmod($backupFolder.
'/'. $endpointBackupFilename, \BX_FILE_PERMISSIONS);
807 $this->
addError(
new Main\
Error(
"Couldn't backup file '{$fullPath}'"));
810 return $isSuccessfull;
823 public function getFileIndex(): Index\FileIndex
825 if (!$this->fileIndex instanceof Index\FileIndex)
827 $indexFileRes = Index\Internals\FileIndexTable::getList([
830 '=FULL_PATH' => $this->
getPath(),
834 $this->fileIndex = $indexFileRes->fetchObject();
837 if (!$this->fileIndex instanceof Index\FileIndex)
839 $this->fileIndex = (
new Index\FileIndex())
840 ->setFullPath($this->
getPath())
844 return $this->fileIndex;
852 public function updatePhraseIndex(): Index\FileIndex
854 $this->getFileIndex();
855 $fileId = $this->fileIndex->getId();
858 $phraseId = Index\Internals\PhraseIndexTable::query()
859 ->registerRuntimeField(
new Main\ORM\Fields\ExpressionField(
'MAXID',
'MAX(%s)', [
'ID']))
864 $pathId = $this->fileIndex->getPathId();
866 $phraseCodeData = [];
867 foreach ($this as $code => $phrase)
871 $phraseCodeData[] = [
873 'FILE_ID' => $fileId,
874 'PATH_ID' => $pathId,
875 'LANG_ID' => $langId,
879 if (!isset($phraseData[$langId]))
881 $phraseData[$langId] = [];
883 $phraseData[$langId][] = [
885 'FILE_ID' => $fileId,
886 'PATH_ID' => $pathId,
893 $filter =
new Translate\Filter([
'fileId' => $fileId]);
895 Index\Internals\PhraseIndexTable::purge($filter);
897 foreach (Translate\Config::getEnabledLanguages() as $langId)
899 $ftsClass = Index\Internals\PhraseFts::getFtsEntityClass($langId);
900 $ftsClass::purge($filter);
904 if (\count($phraseCodeData) > 0)
906 Index\Internals\PhraseIndexTable::bulkAdd($phraseCodeData);
907 foreach ($phraseData as $langId => $phraseLangData)
909 $ftsClass = Index\Internals\PhraseFts::getFtsEntityClass($langId);
910 $ftsClass::bulkAdd($phraseLangData,
'ID');
915 ->setPhraseCount($this->count())
917 ->setIndexedTime(
new Main\
Type\DateTime())
921 return $this->fileIndex;
929 public function deletePhraseIndex(): bool
931 $this->getFileIndex();
932 if ($this->fileIndex->getId() > 0)
934 $filter =
new Translate\Filter([
'id' => $this->fileIndex->getId()]);
936 Index\Internals\FileIndexTable::purge($filter);
938 foreach (Translate\Config::getEnabledLanguages() as $langId)
940 $ftsClass = Index\Internals\PhraseFts::getFtsEntityClass($langId);
941 $ftsClass::purge($filter);
944 unset($this->fileIndex);
955 public function getPhraseIndexCollection(): Index\PhraseIndexCollection
957 $phraseIndexCollection =
new Index\PhraseIndexCollection();
958 foreach ($this->messages as $code => $message)
960 $phraseIndexCollection[] = (
new Index\PhraseIndex)
963 ->setPhrase($message)
967 return $phraseIndexCollection;
983 return isset($this->messages[$code]);
993 public function offsetGet($code): ?string
995 if (isset($this->messages[$code]))
997 return $this->messages[$code];
1011 public function offsetSet($code, $phrase): void
1013 if (!isset($this->messages[$code]))
1015 if ($this->messagesCount ===
null)
1023 $this->messageCodes[] = $code;
1025 $this->messages[$code] = $phrase;
1037 if (isset($this->messages[$code]))
1039 unset($this->messages[$code]);
1041 if (($i = \array_search($code, $this->messageCodes)) !==
false)
1043 unset($this->messageCodes[$i]);
1053 public function sortPhrases()
1055 \ksort($this->messages, \SORT_NATURAL);
1065 public function getPhrases()
1067 return $this->messages;
1074 public function getCodes()
1076 return \is_array($this->messages) ? \array_keys($this->messages) : [];
1084 public function getEnclosure(
string $phraseId): string
1087 if (isset($this->messageEnclosure[$phraseId]))
1089 $enclosure = $this->messageEnclosure[$phraseId];
1104 public function current(): ?string
1106 $code = $this->messageCodes[$this->dataPosition];
1108 if (!isset($this->messages[$code]) || !\is_string($this->messages[$code]) || (empty($this->messages[$code]) && $this->messages[$code] !==
'0'))
1113 return $this->messages[$code];
1121 public function next(): void
1123 ++ $this->dataPosition;
1131 #[\ReturnTypeWillChange]
1132 public function key()
1134 return $this->messageCodes[$this->dataPosition] ?:
null;
1142 public function valid(): bool
1144 return isset($this->messageCodes[$this->dataPosition], $this->messages[$this->messageCodes[$this->dataPosition]]);
1152 public function rewind(): void
1154 $this->dataPosition = 0;
1155 $this->messageCodes = \array_keys($this->messages);
1169 public function count($allowDirectFileAccess =
false): int
1171 if ($this->messagesCount ===
null)
1173 if ($this->messages !==
null && \count($this->messages) > 0)
1177 elseif ($allowDirectFileAccess)
1182 if (\is_array($MESS) && \count($MESS) > 0)