Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT support variables as second and third argument #11521

Merged
merged 13 commits into from
Jan 27, 2025
111 changes: 90 additions & 21 deletions src/i18n/TextCollection/i18nTextCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ protected function getConflicts($entitiesByModule)
// bubble-compare each group of modules
for ($i = 0; $i < count($modules ?? []) - 1; $i++) {
$left = array_keys($entitiesByModule[$modules[$i]] ?? []);
for ($j = $i+1; $j < count($modules ?? []); $j++) {
for ($j = $i + 1; $j < count($modules ?? []); $j++) {
$right = array_keys($entitiesByModule[$modules[$j]] ?? []);
$conflicts = array_intersect($left ?? [], $right);
$allConflicts = array_merge($allConflicts, $conflicts);
Expand Down Expand Up @@ -618,15 +618,55 @@ public function collectFromCode($content, $fileName, Module $module)
$potentialClassName = null;
$currentUse = null;
$currentUseAlias = null;
$inVar = null;
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
$inVarText = '';
$stringVariables = [];
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
foreach ($tokens as $token) {
// Shuffle last token to $lastToken
$previousToken = $thisToken;
$thisToken = $token;

// Track string variables
// Store when reaching end of statement if we have content
if ($token === ";" && $inVar) {
if ($inVarText) {
$stringVariables[$inVar] = $inVarText;
}
$inVar = null;
continue;
}
// End track string variables

// Not all tokens are returned as an array.
// If a token is not variable, but instead it is one particular constant string, it is returned as a string instead.
// You don't get a line number.
// This is the case for braces( "{", "}"), parentheses ("(", ")"), brackets ("[", "]"), comma (","), semi-colon (";")...
if (is_array($token)) {
list($id, $text, $lineNo) = $token;
// minus 2 is used so the the line we get corresponds with what number token_get_all() returned
$line = $lines[$lineNo - 2] ?? '';

// Ignore whitespace
if ($id === T_WHITESPACE) {
continue;
}

// Track string variables
if (!$inTransFn) {
if ($id === T_VARIABLE) {
$inVar = $text;
$inVarText = '';
continue;
}
if ($id === T_CONSTANT_ENCAPSED_STRING && $inVar && $text) {
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
// We need to call process strings because $text is like 'my' or 'string' or "my" or "string"
// This can be called multiple time, eg: $str = 'my' . 'string';
$inVarText .= $this->processString($text);
continue;
}
}
// End track string variables

// Collect use statements so we can get fully qualified class names
// Note that T_USE will match both use statements and anonymous functions with the "use" keyword
// e.g. $func = function () use ($var) { ... };
Expand Down Expand Up @@ -733,6 +773,24 @@ public function collectFromCode($content, $fileName, Module $module)
continue;
}

// Allow _t(Entity.Key, 'translation', $var) and expand _t(Entity.Key, $var)
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
if ($id == T_VARIABLE && !empty($currentEntity)) {
// We have a translation, eg: _t(Entity.Key, 'translation', $var)
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
if (count($currentEntity) == 2) {
continue;
}
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved

// The variable is the second argument or present in the parameter
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
// Try to find it _t(Entity.Key, $var)
$stringValue = $stringVariables[$text] ?? null;

// It's translated, continue
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
if ($stringValue) {
$currentEntity[] = $stringValue;
continue;
}
}

// If inside this translation, some elements might be unreachable
if (in_array($id, [T_VARIABLE, T_STATIC]) ||
($id === T_STRING && in_array($text, ['static', 'parent']))
Expand All @@ -754,26 +812,7 @@ public function collectFromCode($content, $fileName, Module $module)

// Check text
if ($id == T_CONSTANT_ENCAPSED_STRING) {
// Fixed quoting escapes, and remove leading/trailing quotes
if (preg_match('/^\'(?<text>.*)\'$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([\\\\\'])/s', // only \ and '
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} elseif (preg_match('/^\"(?<text>.*)\"$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([nrtvf\\\\$"]|[0-7]{1,3}|\x[0-9A-Fa-f]{1,2})/s', // rich replacement
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} else {
throw new LogicException("Invalid string escape: " . $text);
}
$text = $this->processString($text);
} elseif ($id === T_CLASS_C || $id === T_TRAIT_C) {
// Evaluate __CLASS__ . '.KEY' and i18nTextCollector::class concatenation
$text = implode('\\', $currentClass);
Expand Down Expand Up @@ -881,6 +920,36 @@ function ($input) {
return $entities;
}

/**
* Fixed quoting escapes, and remove leading/trailing quotes
* @throws LogicException if there is no single or double quotes
* @param string $text
* @return string
*/
private function processString(string $text): string
{
if (preg_match('/^\'(?<text>.*)\'$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([\\\\\'])/s', // only \ and '
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} elseif (preg_match('/^\"(?<text>.*)\"$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([nrtvf\\\\$"]|[0-7]{1,3}|\x[0-9A-Fa-f]{1,2})/s', // rich replacement
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} else {
throw new LogicException("Invalid string escape: " . $text);
}
return $text;
}

/**
* Extracts translatables from .ss templates (Self referencing)
*
Expand Down
46 changes: 46 additions & 0 deletions tests/php/i18n/i18nTextCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -974,4 +974,50 @@ public function testModuleFileList()
$this->assertArrayHasKey("{$otherRoot}/code/i18nTestModuleDecorator.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/templates/i18nOtherModule.ss", $otherFiles);
}

public function testItCanCollectVariables()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule');

$php = <<<'PHP'
$concatdouble = "t" . "est" . "";
$concat = 't' . 'e' . 's'
. 't' ;
$str = 'wrong';
$str = 'test';
_t('TestEntity.CONCATDBLKEY', $concatdouble);
_t('TestEntity.CONCATKEY', $concat);
_t('TestEntity.VARKEY', $str);
_t('TestEntity.REGULARKEY', 'test');
PHP;

$collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
$this->assertEquals([
'TestEntity.CONCATDBLKEY' => "test",
'TestEntity.CONCATKEY' => "test",
'TestEntity.VARKEY' => "test",
'TestEntity.REGULARKEY' => "test",
], $collectedTranslatables);
}

public function testItCanUseVariableAsContext()
{
$c = i18nTextCollector::create();
$mymodule = ModuleLoader::inst()->getManifest()->getModule('i18ntestmodule');

$php = <<<'PHP'
$args = ['type' => 'var'];
_t('TestEntity.VARCONTEXT', 'test {type}', $args);
_t('TestEntity.VARIADICCONTEXT', 'test {type}', ...$args);
_t('TestEntity.REGULARCONTEXT', 'test {type}', ['type' => 'var']);
PHP;

$collectedTranslatables = $c->collectFromCode($php, null, $mymodule);
$this->assertEquals([
'TestEntity.VARCONTEXT' => "test {type}",
'TestEntity.VARIADICCONTEXT' => "test {type}",
'TestEntity.REGULARCONTEXT' => "test {type}",
], $collectedTranslatables);
}
}
Loading