19declare(strict_types=1);
26use PHPUnit\Framework\Attributes\PreserveGlobalState;
27use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
28use PHPUnit\Framework\MockObject\Stub;
35#[RunTestsInSeparateProcesses]
36#[PreserveGlobalState(false)]
39 private const string MODULE =
'ptest2';
45 protected function setUp(): void
49 if (!defined(
'ILIAS_ABSOLUTE_PATH')) {
50 define(
'ILIAS_ABSOLUTE_PATH', realpath(__DIR__ .
'/../../../../'));
55 (
new ReflectionClass(ilCachedLanguage::class))->getProperty(
'instances')->setValue(
null, []);
60 if ($this->fixture_directory !==
null) {
64 if ($this->created_client_data_dir_root && defined(
'CLIENT_DATA_DIR') && is_dir(
CLIENT_DATA_DIR)) {
74 $this->fixture_directory =
'tmp-savevalues-plural-fixtures-' . bin2hex(random_bytes(4));
75 $relative_path =
'components/ILIAS/Language/tests/' . $this->fixture_directory .
'/';
78 $catalog->setHeader(
'Content-Type',
'text/plain; charset=UTF-8');
79 $catalog->setHeader(
'Plural-Forms',
'nplurals=2; plural=(n != 1);');
81 $entry->setPlural(
'items', [
'Eintrag',
'Einträge']);
82 $catalog->add($entry);
87 LanguageFileDirectoryManager::class,
96 $lng = (
new ReflectionClass(ilLanguage::class))->newInstanceWithoutConstructor();
104 $fake_global_file = (
new ReflectionClass(ilLanguageFile::class))->newInstanceWithoutConstructor();
105 $fake_global_file->setAllValues([]);
106 $fake_global_file->setAllComments([]);
108 $cache = (
new ReflectionClass(ilLanguageFile::class))->getProperty(
'global_file_objects');
109 $existing = $cache->isInitialized() ? $cache->getValue() : [];
110 $cache->setValue(
null, $existing + [$lang_key => $fake_global_file]);
121 $lng_modules_stmt = $this->createStub(ilDBStatement::class);
122 $remarks_stmt = $this->createStub(ilDBStatement::class);
123 $lng_data_stmt = $this->createStub(ilDBStatement::class);
124 $query_f_stmt = $this->createStub(ilDBStatement::class);
126 $db = $this->createStub(ilDBInterface::class);
127 $db->method(
'quote')->willReturnCallback(
static fn($value,
string $type =
''):
string =>
"'" . (
string) $value .
"'");
128 $db->method(
'in')->willReturn(
"module IN ('placeholder')");
129 $db->method(
'query')->willReturnCallback(
130 static function (
string $sql) use ($lng_modules_stmt, $remarks_stmt, $lng_data_stmt):
ilDBStatement {
131 if (str_contains($sql,
'lng_modules')) {
132 return $lng_modules_stmt;
134 return str_contains($sql,
'remarks') ? $remarks_stmt : $lng_data_stmt;
137 $db->method(
'queryF')->willReturn($query_f_stmt);
138 $remaining_remarks = $existing_remarks;
139 $db->method(
'fetchAssoc')->willReturnCallback(
140 static function (
ilDBStatement $statement) use ($query_f_stmt, $remarks_stmt, &$remaining_remarks): ?array {
141 if ($statement === $query_f_stmt) {
142 return [
'lang_array' => serialize([])];
144 if ($statement === $remarks_stmt) {
145 return array_shift($remaining_remarks) ?:
null;
150 $db->method(
'manipulate')->willReturn(0);
151 $db->method(
'insert')->willReturn(1);
153 $db->method(
'replace')->willReturnCallback(
154 static function (
string $table, array $keys, array $values) use (&$replace_calls):
int {
155 if ($table ===
'lng_data') {
156 $replace_calls[] = [$keys[
'identifier'][1] ??
null, [
157 'value' => $values[
'value'][1] ??
null,
158 'local_change' => $values[
'local_change'][1] ??
null,
159 'remarks' => $values[
'remarks'][1] ??
null,
182 [self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU'],
186 $this->assertSame([], $unwritten,
'the overlay of the migrated module must be written');
187 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
188 $this->assertCount(1, $item_calls,
'exactly one lng_data row for the plural message, not one per form');
189 $this->assertSame(
'Einträge NEU', $item_calls[0][1][
'value']);
190 $this->assertNotNull($item_calls[0][1][
'local_change'],
'marked as a local change');
204 [self::MODULE .
$lng->separator .
'item' =>
'X'],
205 [self::MODULE .
$lng->separator .
'item' =>
'Bitte prüfen']
208 $this->assertSame([], $unwritten);
209 $overlay = TranslationCatalog::fromPoFile(
210 rtrim(
CLIENT_DATA_DIR,
'/') .
'/lang/' . self::MODULE .
'/' . self::LANG_KEY .
'/' . self::MODULE .
'_' . self::LANG_KEY .
'.po'
212 $entry = $overlay->find(
null,
'item');
213 $this->assertNotNull($entry);
214 $this->assertSame([
'Eintrag',
'X'], $entry->getPluralTranslations(),
'form [0] unchanged, form [1] (default) is the plain value');
217 \
ILIAS\Language\ComponentTranslation\LocalChangeComments::getRemark($entry)
219 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
220 $this->assertCount(1, $item_calls);
221 $this->assertSame(
'X', $item_calls[0][1][
'value']);
222 $this->assertNotNull($item_calls[0][1][
'local_change']);
237 self::MODULE .
$lng->separator .
'item' =>
'Von Plain-Key',
238 self::MODULE .
$lng->separator .
'item [1]' =>
'Von Formzeile',
243 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
244 $this->assertCount(1, $item_calls);
245 $this->assertSame(
'Von Formzeile', $item_calls[0][1][
'value']);
260 self::MODULE .
$lng->separator .
'item [1]' =>
'Von Formzeile',
261 self::MODULE .
$lng->separator .
'item' =>
'Von Plain-Key',
266 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
267 $this->assertCount(1, $item_calls);
268 $this->assertSame(
'Von Plain-Key', $item_calls[0][1][
'value']);
277 $replace_calls_1 = [];
281 [self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU'],
285 $replace_calls_2 = [];
290 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag',
291 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge',
296 $item_calls = array_values(array_filter($replace_calls_2,
static fn(array $call):
bool => $call[0] ===
'item'));
297 $this->assertCount(1, $item_calls);
298 $this->assertSame(
'Einträge', $item_calls[0][1][
'value']);
299 $this->assertNull($item_calls[0][1][
'local_change'],
'reset to the shipped value is not a local change');
314 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag NEU',
315 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU',
317 [self::MODULE .
$lng->separator .
'item [0]' =>
'', self::MODULE .
$lng->separator .
'item [1]' =>
'Bemerkung zu Form 1'],
320 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
321 $this->assertCount(1, $item_calls);
322 $this->assertSame(
'Bemerkung zu Form 1', $item_calls[0][1][
'remarks']);
333 [
'module' => self::MODULE,
'identifier' =>
'item',
'remarks' =>
'Alte Bemerkung'],
339 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag NEU',
340 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge',
342 [self::MODULE .
$lng->separator .
'item [0]' =>
''],
345 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
346 $this->assertCount(1, $item_calls);
347 $this->assertNull($item_calls[0][1][
'remarks']);
358 [
'module' => self::MODULE,
'identifier' =>
'item',
'remarks' =>
'Bestehende Bemerkung'],
364 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag',
365 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU',
367 [self::MODULE .
$lng->separator .
'item [1]' =>
'Bestehende Bemerkung'],
370 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
371 $this->assertCount(1, $item_calls);
372 $this->assertSame(
'Bestehende Bemerkung', $item_calls[0][1][
'remarks']);
387 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag',
388 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge',
390 [self::MODULE .
$lng->separator .
'item [1]' =>
'Neue Bemerkung'],
393 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
394 $this->assertCount(1, $item_calls,
'the row must be written for the remark alone');
395 $this->assertSame(
'Einträge', $item_calls[0][1][
'value']);
396 $this->assertNull($item_calls[0][1][
'local_change'],
'the value itself did not change');
397 $this->assertSame(
'Neue Bemerkung', $item_calls[0][1][
'remarks']);
412 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag NEU',
413 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge',
415 [self::MODULE .
$lng->separator .
'item [1]' =>
'Bemerkung aus Form 1'],
418 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
419 $this->assertCount(1, $item_calls);
420 $this->assertSame(
'Einträge', $item_calls[0][1][
'value'],
'the default form (1) itself did not change');
421 $this->assertNull($item_calls[0][1][
'local_change']);
422 $this->assertSame(
'Bemerkung aus Form 1', $item_calls[0][1][
'remarks']);
437 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU',
438 self::MODULE .
$lng->separator .
'item [5]' =>
'Geister-Form',
443 $this->assertSame([], $unwritten);
444 $item_calls = array_values(array_filter($replace_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
445 $this->assertCount(1, $item_calls,
'exactly one row for the real message - the orphan form must not create its own');
446 $this->assertSame(
'Einträge NEU', $item_calls[0][1][
'value']);
459 [self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU'],
462 $overlay = TranslationCatalog::fromPoFile(
463 rtrim(
CLIENT_DATA_DIR,
'/') .
'/lang/' . self::MODULE .
'/' . self::LANG_KEY .
'/' . self::MODULE .
'_' . self::LANG_KEY .
'.po'
465 $this->assertNotNull($overlay->find(
null,
'item'),
'precondition: the overlay holds the local change');
471 $this->assertSame([], $unwritten);
472 $item_calls = array_values(array_filter($delete_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
473 $this->assertCount(1, $item_calls,
'exactly one merged row, not a separate row per form');
474 $this->assertSame(
'Einträge', $item_calls[0][1][
'value'],
'back to the shipped default form value');
475 $this->assertNull($item_calls[0][1][
'local_change']);
477 $overlay_after_path = rtrim(
CLIENT_DATA_DIR,
'/') .
'/lang/' . self::MODULE .
'/' . self::LANG_KEY .
'/' . self::MODULE .
'_' . self::LANG_KEY .
'.po';
478 $this->assertFalse(is_file($overlay_after_path),
'no local change left at all - the whole overlay is removed');
492 self::MODULE .
$lng->separator .
'item [0]' =>
'Eintrag NEU',
493 self::MODULE .
$lng->separator .
'item [1]' =>
'Einträge NEU',
500 [
'module' => self::MODULE,
'identifier' =>
'item',
'remarks' =>
'Alte Bemerkung'],
504 $item_calls = array_values(array_filter($delete_calls,
static fn(array $call):
bool => $call[0] ===
'item'));
505 $this->assertCount(1, $item_calls);
506 $this->assertSame(
'Alte Bemerkung', $item_calls[0][1][
'remarks']);
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
One message of a TranslationCatalog (a msgctxt/msgid/msgstr block of a .po file).
static removeShippedDirectory(string $relative_path)
static ensureClientDataDirDefinedOrSkip(\PHPUnit\Framework\TestCase $test)
Guards every fixture that writes under CLIENT_DATA_DIR: a full-suite run may have defined it as the r...
static directory(string $prefix, string $path, bool $local=false)
static removeDirectory(string $directory)
static writeShippedPo(string $relative_path, string $module, string $lang_key, TranslationCatalog $catalog)
Writes $catalog as the shipped .po: a module only counts as migrated while that exists.
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...
Class ilLanguageBaseTest.
setGlobalVariable(string $name, $value)
static _deleteValues(string $a_lang_key, array $a_values=array())
Delete a set of translation in the database.
static _saveValues(string $a_lang_key, array $a_values=array(), array $a_remarks=array())
Save a set of translation in the database.
A directory of language files with a path and a prefix for them.
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.