19declare(strict_types=1);
25use PHPUnit\Framework\Attributes\DataProvider;
26use PHPUnit\Framework\TestCase;
49 $now = new \DateTimeImmutable(
'2026-09-21T10:00:00Z');
63 new \DateTimeImmutable(
'2026-09-21T10:00:00Z')
70 new \DateTimeImmutable(
'2026-09-21T14:00:00Z')
86 new \DateTimeImmutable(
'2026-09-21T10:00:00Z')
94 new \DateTimeImmutable(
'2026-09-21T12:00:00Z')
108 new \DateTimeImmutable(
'2026-09-21T10:00:00Z')
114 'Hallo, nochmal geändert',
115 new \DateTimeImmutable(
'2026-09-21T12:00:00Z')
148 $localChangeComments = array_values(array_filter(
149 $translation->getTranslatorComments(),
150 static fn(
string $comment):
bool => str_starts_with(
$comment,
'local_change: ')
153 $this->assertCount(1, $localChangeComments);
154 $this->assertSame(
'local_change: 2026-09-21T12:00:00Z', $localChangeComments[0]);
161 #[DataProvider('valuesNeedingEscaping')]
164 string $expected_comment
169 $this->assertSame([$expected_comment], $entry->getTranslatorComments());
176 'LF' => [
"Zeile eins\nZeile zwei",
'original_escaped: Zeile eins\\nZeile zwei'],
177 'CRLF' => [
"Zeile eins\r\nZeile zwei",
'original_escaped: Zeile eins\\r\\nZeile zwei'],
178 'lone CR' => [
"a\rb",
'original_escaped: a\\rb'],
179 'trailing LF' => [
"Hallo\n",
'original_escaped: Hallo\\n'],
180 'backslash' => [
'C:\\pfad',
'original_escaped: C:\\\\pfad'],
182 'literal backslash-n' => [
'a\\nb',
'original_escaped: a\\\\nb'],
183 'literal backslash before a real LF' => [
"a\\\nb",
'original_escaped: a\\\\\\nb'],
184 'double backslash' => [
'a\\\\b',
'original_escaped: a\\\\\\\\b'],
185 'only a line break' => [
"\n",
'original_escaped: \\n'],
191 return self::valuesNeedingEscaping() + [
192 'leading and trailing spaces' => [
' Hallo ',
'original: Hallo '],
193 'trailing tab' => [
"Hallo\t",
"original: Hallo\t"],
194 'only a space' => [
' ',
'original: '],
195 'empty' => [
'',
'original: '],
196 'zero' => [
'0',
'original: 0'],
197 'escaped with surrounding spaces' => [
" a\nb ",
'original_escaped: a\\nb '],
210 $this->assertSame([
'original: Hallo "Welt" %s #:# ###'], $entry->getTranslatorComments());
217 #[DataProvider('legacyOriginalComments')]
221 $entry->addTranslatorComment(
$comment);
229 'flattened line break' => [
'original: a b',
'a b'],
230 'unescaped backslash' => [
'original: C:\\pfad',
'C:\\pfad'],
231 'backslash-n stays two characters' => [
'original: a\\nb',
'a\\nb'],
242 $entry->addTranslatorComment(
'original: C:\\pfad');
248 $this->assertSame([
'original_escaped: C:\\\\pfad'], $entry->getTranslatorComments());
259 $entry->addTranslatorComment(
'translator note');
263 $this->assertSame([
'translator note',
'original: einzeilig'], $entry->getTranslatorComments());
267 $this->assertSame([
'translator note',
'original_escaped: wieder\\nmehrzeilig'], $entry->getTranslatorComments());
279 $entry->addTranslatorComment(
'translator note');
283 $this->assertSame([
'original_escaped: Zeile\\r\\nzwei',
'translator note'], $entry->getTranslatorComments());
293 $entry->addTranslatorComment(
'original: Hallo');
294 $entry->addTranslatorComment(
'original_escaped: Alt\\nZeile');
298 $this->assertSame([
'original: Hallo'], $entry->getTranslatorComments());
311 LocalChangeComments::refresh($entry,
"Zeile eins\r\nZeile zwei",
"Zeile eins\r\nZeile zwei",
new \DateTimeImmutable(
'2026-09-21T10:00:00Z'));
314 LocalChangeComments::refresh($entry,
"Zeile eins\r\nZeile zwei",
"Zeile eins\nZeile zwei",
new \DateTimeImmutable(
'2026-09-21T11:00:00Z'));
325 #[DataProvider('valuesSurvivingAPoFileRoundTrip')]
329 $entry->translate($value);
331 $entry->addTranslatorComment(
'local_change: 2026-09-21T10:00:00Z');
333 $catalog->add($entry);
337 $this->assertNotNull($parsed);
338 $this->assertSame([$expected_comment,
'local_change: 2026-09-21T10:00:00Z'], $parsed->getTranslatorComments());
340 $this->assertSame($value, $parsed->getTranslation());
351 $entry->addTranslatorComment(
'translator note');
355 $this->assertSame([
'original: Hallo',
'translator note'], $entry->getTranslatorComments());
362 $entry->addTranslatorComment(
'translator note');
366 $this->assertSame([
'translator note',
'original: Servus'], $entry->getTranslatorComments());
406 $entry->addTranslatorComment(
'original: Hallo');
407 $entry->addTranslatorComment(
'translator note');
408 $entry->addTranslatorComment(
'original_escaped: Zeile\\nzwei');
413 $this->assertSame([
'translator note'], $entry->getTranslatorComments());
419 $entry->addTranslatorComment(
'translator note');
427 [
'translator note',
'local_change: 2026-09-21T10:00:00Z'],
428 $entry->getTranslatorComments()
455 $entry->addTranslatorComment(
'local_change: yesterday');
473 $this->assertSame([
'remark: Bitte prüfen'], $entry->getTranslatorComments());
481 #[DataProvider('valuesNeedingEscaping')]
487 $comments = $entry->getTranslatorComments();
489 $this->assertStringStartsWith(
'remark_escaped: ',
$comments[0]);
515 $this->assertSame([], $entry->getTranslatorComments());
536 $this->expectException(\InvalidArgumentException::class);
556 $this->assertTrue(mb_check_encoding((
string) $stored,
'UTF-8'),
'no character was split in half');
580 $this->assertSame([
'remark: ohne Umbruch'], $entry->getTranslatorComments());
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
static fromPoString(string $content)
One message of a TranslationCatalog (a msgctxt/msgid/msgstr block of a .po file).