mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-11-05 00:52:57 -05:00
* Add English references to DIRTY translation comments Translators currently have to find the English source separately for each `// DIRTY` entry. The translation formatter now appends it on the same line, including English plural fallback for languages with additional plural forms. It keeps hints on one safe PHP comment line and refreshes them when English changes. Only comments change in the locale files; translation values and plural rules are unchanged. Single-form translations show both English forms as `Singular: … | Plural: …`. Generated README contribution links match bare and annotated TODO/DIRTY markers and retain the end-of-line anchor. Closes #9463. * Show both English forms for single-form translations
264 lines
8.0 KiB
PHP
Executable File
264 lines
8.0 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/_cli.php';
|
|
require_once __DIR__ . '/i18n/I18nCompletionValidator.php';
|
|
require_once __DIR__ . '/i18n/I18nData.php';
|
|
require_once __DIR__ . '/i18n/I18nFile.php';
|
|
require_once __DIR__ . '/i18n/I18nUsageValidator.php';
|
|
require_once dirname(__DIR__) . '/constants.php';
|
|
|
|
$cliOptions = new class extends CliOptionsParser {
|
|
/** @var array<int,string> $language */
|
|
public array $language;
|
|
public bool $displayResult;
|
|
public bool $help;
|
|
public bool $displayReport;
|
|
public bool $generateReadme;
|
|
|
|
public function __construct() {
|
|
$this->addOption('language', (new CliOption('language', 'l'))->typeOfArrayOfString());
|
|
$this->addOption('displayResult', (new CliOption('display-result', 'd'))->withValueNone());
|
|
$this->addOption('help', (new CliOption('help', 'h'))->withValueNone());
|
|
$this->addOption('displayReport', (new CliOption('display-report', 'r'))->withValueNone());
|
|
$this->addOption('generateReadme', (new CliOption('generate-readme', 'g'))->withValueNone());
|
|
parent::__construct();
|
|
}
|
|
};
|
|
|
|
if (!empty($cliOptions->errors)) {
|
|
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
|
|
}
|
|
if ($cliOptions->help) {
|
|
checkHelp();
|
|
}
|
|
|
|
$i18nFile = new I18nFile();
|
|
$i18nData = new I18nData($i18nFile->load());
|
|
|
|
if (!empty($cliOptions->language)) {
|
|
$languages = $cliOptions->language;
|
|
} else {
|
|
$languages = $i18nData->getAvailableLanguages();
|
|
}
|
|
|
|
$isValidated = true;
|
|
$languageNameIssues = $i18nData->validateLanguageNames();
|
|
foreach ($languageNameIssues as $issue) {
|
|
fwrite(STDERR, "Error: {$issue}\n");
|
|
$isValidated = false;
|
|
}
|
|
|
|
$result = [];
|
|
$report = [];
|
|
$percentage = [];
|
|
|
|
foreach ($languages as $language) {
|
|
if ($language === $i18nData::REFERENCE_LANGUAGE) {
|
|
$usedTranslations = findUsedTranslations();
|
|
$referenceLanguage = $i18nData->getReferenceLanguage();
|
|
$pluralFamilies = loadPluralReferenceFamilies($referenceLanguage);
|
|
if ($pluralFamilies !== []) {
|
|
$referenceLanguage['plurals.php'] = $pluralFamilies;
|
|
}
|
|
$i18nValidator = new I18nUsageValidator($referenceLanguage, $usedTranslations['keys'], $usedTranslations['prefixes']);
|
|
} else {
|
|
$i18nValidator = new I18nCompletionValidator($i18nData->getReferenceLanguage(), $i18nData->getLanguage($language));
|
|
}
|
|
$isValidated = $i18nValidator->validate() && $isValidated;
|
|
|
|
$report[$language] = sprintf('%-5s - %s', $language, $i18nValidator->displayReport());
|
|
$percentage[$language] = $i18nValidator->displayReport(percentage_only: true);
|
|
$result[$language] = $i18nValidator->displayResult();
|
|
}
|
|
|
|
if ($cliOptions->displayResult) {
|
|
foreach ($result as $lang => $value) {
|
|
echo 'Language: ', $lang, PHP_EOL;
|
|
print_r($value);
|
|
echo PHP_EOL;
|
|
}
|
|
}
|
|
|
|
if ($cliOptions->displayReport) {
|
|
foreach ($report as $value) {
|
|
echo $value;
|
|
}
|
|
}
|
|
|
|
function writeToReadme(string $readmePath, string $markdownTable): void {
|
|
$language = explode('.', $readmePath)[1];
|
|
// expecting `README.md` for `en` or `README.fr.md` for `fr`
|
|
if ($language === 'md') {
|
|
$language = 'en';
|
|
}
|
|
Minz_Translate::init($language);
|
|
$placeholders = [];
|
|
if (preg_match_all('/__.*?__/', $markdownTable, $placeholders) === false) {
|
|
echo 'Error: Fail while matching translation placeholders', PHP_EOL;
|
|
exit(1);
|
|
}
|
|
foreach (array_unique($placeholders[0]) as $_ => $placeholder) {
|
|
$markdownTable = str_replace($placeholder, _t('gen.readme.' . substr($placeholder, 2, -2)), $markdownTable);
|
|
}
|
|
|
|
$readme = file_get_contents($readmePath);
|
|
if ($readme === false) {
|
|
echo 'Error: Unable to open ' . $readmePath, PHP_EOL;
|
|
exit(1);
|
|
}
|
|
if (file_put_contents($readmePath, preg_replace('/<translations>(.*?)<\/translations>/s', <<<EOF
|
|
<translations>
|
|
<!-- This section is automatically generated by `./cli/check.translation.php -g` -->
|
|
|
|
$markdownTable
|
|
|
|
</translations>
|
|
EOF, $readme)) === false) {
|
|
echo 'Error: Fail while writing to ' . $readmePath, PHP_EOL;
|
|
exit(1);
|
|
}
|
|
echo 'Successfully written translation status into ' . $readmePath, PHP_EOL;
|
|
}
|
|
|
|
if ($cliOptions->generateReadme) {
|
|
if ($languageNameIssues !== []) {
|
|
// Refuse to regenerate the README when language directory names and
|
|
// `gen.lang.*` keys disagree, otherwise we would silently produce a
|
|
// corrupt translation table (e.g. literal `gen.lang.*` keys instead of
|
|
// localised language names). Routine incomplete translations are fine.
|
|
exit(1);
|
|
}
|
|
$markdownTable = <<<EOF
|
|
| __language__ | __translated__ | |
|
|
| - | - | - |
|
|
EOF;
|
|
$markdownTable .= "\n";
|
|
|
|
foreach ($percentage as $lang => $value) {
|
|
$percentageInt = intval(rtrim($value, '%'));
|
|
$completed = intval($percentageInt / 10);
|
|
$uncompleted = intval(ceil((100 - $percentageInt) / 10));
|
|
$progressBar = str_repeat('■', $completed) . str_repeat('・', $uncompleted);
|
|
|
|
$ghSearchUrl = 'https://github.com/search?q=' . urlencode("repo:FreshRSS/FreshRSS path:app/i18n/$lang /(TODO|DIRTY)( - .*)?$/");
|
|
|
|
$markdownTable .= '| ' . implode(' | ', [
|
|
_t('gen.lang.' . $lang) . " ($lang)",
|
|
$progressBar . ' ' . $percentageInt . '%',
|
|
"[__contribute__]($ghSearchUrl)",
|
|
]) . " |\n";
|
|
}
|
|
// In case we're located in ./cli/
|
|
if (!file_exists('constants.php')) {
|
|
chdir('..');
|
|
}
|
|
foreach (array_merge(['README.md'], glob('README.*.md') ?: []) as $readmePath) {
|
|
writeToReadme($readmePath, rtrim($markdownTable));
|
|
}
|
|
exit();
|
|
}
|
|
|
|
if (!$isValidated) {
|
|
exit(1);
|
|
}
|
|
|
|
/**
|
|
* Find used translation keys in the project
|
|
*
|
|
* Iterates through all php and phtml files in the whole project and extracts all
|
|
* translation keys used.
|
|
*
|
|
* @return array{keys:list<string>,prefixes:list<string>}
|
|
*/
|
|
function findUsedTranslations(): array {
|
|
$directory = new RecursiveDirectoryIterator(__DIR__ . '/..', FilesystemIterator::SKIP_DOTS);
|
|
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::LEAVES_ONLY, RecursiveIteratorIterator::CATCH_GET_CHILD);
|
|
$regex = new RegexIterator($iterator, '/^.+\.(php|phtml)$/i', RecursiveRegexIterator::GET_MATCH);
|
|
$usedI18n = [];
|
|
$usedPrefixes = [];
|
|
foreach ($regex as $file => $value) {
|
|
if (!is_string($file) || $file === '') {
|
|
continue;
|
|
}
|
|
$fileContent = file_get_contents($file);
|
|
if ($fileContent === false) {
|
|
continue;
|
|
}
|
|
preg_match_all('/_t\([\'"](?P<strings>[^\'"]+)[\'"]/', $fileContent, $matches);
|
|
$usedI18n = array_merge($usedI18n, $matches['strings']);
|
|
preg_match_all('/Minz_Translate::plural\(\s*[\'"](?P<string>[^\'"]+)[\'"](?P<dynamic>\s*\.)?/', $fileContent, $pluralMatches, PREG_SET_ORDER);
|
|
foreach ($pluralMatches as $match) {
|
|
$string = $match['string'];
|
|
if (($match['dynamic'] ?? '') !== '') {
|
|
$usedPrefixes[] = $string;
|
|
} else {
|
|
$usedI18n[] = $string;
|
|
}
|
|
}
|
|
}
|
|
return [
|
|
'keys' => array_values(array_unique($usedI18n)),
|
|
'prefixes' => array_values(array_unique($usedPrefixes)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string,array<string,I18nValue>> $referenceLanguage
|
|
* @return array<string,I18nValue>
|
|
*/
|
|
function loadPluralReferenceFamilies(array $referenceLanguage): array {
|
|
$pluralFamilies = [];
|
|
foreach ($referenceLanguage as $values) {
|
|
foreach ($values as $key => $value) {
|
|
if (preg_match('/^(?P<base>.+)\.(?P<index>\d+)$/', $key, $matches) !== 1) {
|
|
continue;
|
|
}
|
|
$baseKey = $matches['base'];
|
|
$index = $matches['index'];
|
|
$pluralFamilies[$baseKey][(int)$index] = $value->__toString();
|
|
}
|
|
}
|
|
|
|
$normalisedFamilies = [];
|
|
foreach ($pluralFamilies as $baseKey => $messageFamily) {
|
|
$messages = [];
|
|
ksort($messageFamily);
|
|
foreach ($messageFamily as $message) {
|
|
if ($message !== '') {
|
|
$messages[] = $message;
|
|
}
|
|
}
|
|
|
|
$normalisedFamilies[$baseKey] = new I18nValue(implode(' | ', $messages));
|
|
}
|
|
|
|
return $normalisedFamilies;
|
|
}
|
|
|
|
/**
|
|
* Output help message.
|
|
*/
|
|
function checkHelp(): never {
|
|
$file = str_replace(__DIR__ . '/', '', __FILE__);
|
|
|
|
echo <<<HELP
|
|
NAME
|
|
$file
|
|
|
|
SYNOPSIS
|
|
php $file [OPTION]...
|
|
|
|
DESCRIPTION
|
|
Check if translation files have missing keys or missing translations.
|
|
|
|
-d, --display-result display results.
|
|
-h, --help display this help and exit.
|
|
-l, --language=LANG filter by LANG.
|
|
-r, --display-report display completion report.
|
|
-g, --generate-readme generate translation progress section in readme.
|
|
|
|
HELP;
|
|
exit();
|
|
}
|