Scripts: Relax rich-text checks in the translation validator

And fix a bunch of pre-existing errors.
This commit is contained in:
Stenzek
2026-07-04 21:44:17 +10:00
parent fb994670e6
commit 674af5152a
14 changed files with 151 additions and 46 deletions
@@ -19,9 +19,11 @@ from translation.ts_utils import ( # noqa: E402
extract_placeholders,
extract_rich_tags,
parse_catalog,
placeholder_counts_match,
placeholders_are_subset,
placeholders_match,
replace_translation_node,
unbalanced_rich_tags,
validate_translation,
)
@@ -94,13 +96,21 @@ class TranslationToolTests(unittest.TestCase):
self.assertEqual(1, placeholders["qt:%1"])
self.assertEqual(1, placeholders["qt:%n"])
self.assertEqual(1, placeholders["fmt:{}"])
self.assertEqual(1, placeholders["fmt:{0:08X}"])
self.assertEqual(1, placeholders["fmt:{0}"])
self.assertEqual(1, placeholders["printf:%.1f"])
self.assertEqual(1, placeholders["printf:%s"])
self.assertEqual(1, placeholders["template:${title}"])
self.assertEqual(7, sum(placeholders.values()))
self.assertTrue(placeholders_match("{} {}", "{1} {0}"))
self.assertFalse(placeholders_match("{} {}", "{0} {0}"))
self.assertTrue(placeholders_match("Use {0}, then use {0} again", "Usar {0}"))
self.assertFalse(placeholder_counts_match("Use {0}, then use {0} again", "Usar {0}"))
self.assertTrue(placeholders_match("Saved at {0:%H:%M} on {0:%Y/%m/%d}", "Guardado {0:%d/%m/%Y}"))
self.assertFalse(
placeholder_counts_match("Saved at {0:%H:%M} on {0:%Y/%m/%d}", "Guardado {0:%d/%m/%Y}")
)
self.assertFalse(placeholder_counts_match("{0} {0} {1}", "{0} {1} {1}"))
self.assertFalse(placeholders_match("Use %1 and %2", "Usar %1"))
self.assertTrue(placeholders_are_subset("{} of %n", "%n"))
self.assertFalse(placeholders_are_subset("{} of %n", "%n %1"))
self.assertEqual({"qt:%1": 1}, dict(extract_placeholders("%1x")))
@@ -108,7 +118,12 @@ class TranslationToolTests(unittest.TestCase):
def test_rich_text_ignores_angle_bracket_labels(self) -> None:
self.assertFalse(extract_rich_tags("<Parent Directory>"))
self.assertEqual({"strong": 1, "/strong": 1}, dict(extract_rich_tags("<strong>Text</strong>")))
self.assertTrue(validate_translation("<strong>Text</strong>", "テキスト"))
self.assertFalse(validate_translation("<strong>Text</strong>", "テキスト"))
def test_rich_text_tag_pairs_are_balanced(self) -> None:
self.assertFalse(unbalanced_rich_tags("<html><head/><body><br><hr/></body></html>"))
self.assertFalse(unbalanced_rich_tags("<strong><b>Text</strong></b>"))
self.assertEqual({"p": (2, 1), "strong": (1, 0)}, unbalanced_rich_tags("<p><p><strong>Text</p>"))
def test_fingerprint_ignores_translation_changes(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@@ -256,6 +271,24 @@ class TranslationToolTests(unittest.TestCase):
self.assertIn(f"{catalog}:{expected_line}:", result.stdout)
self.assertIn("[source: alpha.cpp:1]", result.stdout)
def test_validation_warns_for_placeholder_count_mismatch(self) -> None:
with tempfile.TemporaryDirectory() as directory:
catalog = Path(directory) / "catalog.ts"
catalog.write_text(
FIXTURE.replace(
"Hello %1 {0} ${title} %.1f &lt;strong&gt;world&lt;/strong&gt;",
"Use {0}, then use {0} again",
).replace(
'<translation type="unfinished"></translation>',
"<translation>Usar {0}</translation>",
1,
),
encoding="utf-8",
)
result = self.run_tool("validate_ts.py", catalog, "--placeholders-only")
self.assertIn("WARNING:", result.stdout)
self.assertIn("placeholder count mismatch", result.stdout)
if __name__ == "__main__":
unittest.main()
+64 -13
View File
@@ -152,7 +152,10 @@ def extract_placeholders(text: str) -> collections.Counter[str]:
if any(match.start() < end and match.end() > start for start, end in occupied):
continue
occupied.append(match.span())
tokens.append(f"{kind}:{match.group(0)}")
placeholder = match.group(0)
if kind == "fmt" and ":" in placeholder:
placeholder = f"{placeholder.split(':', 1)[0]}}}"
tokens.append(f"{kind}:{placeholder}")
return collections.Counter(tokens)
@@ -174,9 +177,12 @@ def placeholders_match(source: str, translation: str) -> bool:
translated_non_fmt = collections.Counter(
token for token in translated_tokens.elements() if not token.startswith("fmt:")
)
if source_non_fmt != translated_non_fmt:
if set(source_non_fmt) != set(translated_non_fmt):
return False
if set(source_fmt) == set(translated_fmt):
return True
# Qt's fmt usage permits translators to number anonymous plain placeholders
# when reordering arguments, e.g. "{} {}" -> "{1} {0}".
if set(source_fmt) == {"{}"}:
@@ -185,13 +191,14 @@ def placeholders_match(source: str, translation: str) -> bool:
return False
def placeholders_are_subset(source: str, translation: str) -> bool:
"""Allow a plural form to omit redundant arguments without adding any."""
if placeholders_match(source, translation):
return True
def placeholder_counts_match(source: str, translation: str) -> bool:
"""Return whether compatible placeholders occur the same number of times."""
if not placeholders_match(source, translation):
return False
source_tokens = extract_placeholders(source)
translated_tokens = extract_placeholders(translation)
if translated_tokens <= source_tokens:
if source_tokens == translated_tokens:
return True
source_fmt = collections.Counter(
@@ -206,7 +213,35 @@ def placeholders_are_subset(source: str, translation: str) -> bool:
translated_non_fmt = collections.Counter(
token for token in translated_tokens.elements() if not token.startswith("fmt:")
)
if not translated_non_fmt <= source_non_fmt or set(source_fmt) != {"{}"}:
if set(source_fmt) == {"{}"}:
fmt_counts_match = sum(source_fmt.values()) == sum(translated_fmt.values())
else:
fmt_counts_match = source_fmt == translated_fmt
return source_non_fmt == translated_non_fmt and fmt_counts_match
def placeholders_are_subset(source: str, translation: str) -> bool:
"""Allow a plural form to omit redundant arguments without adding any."""
if placeholders_match(source, translation):
return True
source_tokens = extract_placeholders(source)
translated_tokens = extract_placeholders(translation)
if set(translated_tokens) <= set(source_tokens):
return True
source_fmt = collections.Counter(
token.removeprefix("fmt:") for token in source_tokens.elements() if token.startswith("fmt:")
)
translated_fmt = collections.Counter(
token.removeprefix("fmt:") for token in translated_tokens.elements() if token.startswith("fmt:")
)
source_non_fmt = collections.Counter(
token for token in source_tokens.elements() if not token.startswith("fmt:")
)
translated_non_fmt = collections.Counter(
token for token in translated_tokens.elements() if not token.startswith("fmt:")
)
if not set(translated_non_fmt) <= set(source_non_fmt) or set(source_fmt) != {"{}"}:
return False
allowed_numbered = {f"{{{index}}}" for index in range(source_fmt["{}"])}
return set(translated_fmt) <= allowed_numbered and all(count == 1 for count in translated_fmt.values())
@@ -220,6 +255,23 @@ def extract_rich_tags(text: str) -> collections.Counter[str]:
return collections.Counter(tags)
def unbalanced_rich_tags(text: str) -> dict[str, tuple[int, int]]:
"""Return paired rich-text elements with unequal opening and closing counts."""
opening: collections.Counter[str] = collections.Counter()
closing: collections.Counter[str] = collections.Counter()
for match in RICH_TAG_RE.finditer(text):
tag = match.group(2).lower()
if tag in {"br", "hr"} or (not match.group(1) and match.group(0).rstrip().endswith("/>")):
continue
(closing if match.group(1) else opening)[tag] += 1
return {
tag: (opening[tag], closing[tag])
for tag in sorted(opening.keys() | closing.keys())
if opening[tag] != closing[tag]
}
def validate_translation(source: str, translation: str, allow_missing_placeholders: bool = False) -> list[str]:
problems: list[str] = []
source_placeholders = extract_placeholders(source)
@@ -233,11 +285,6 @@ def validate_translation(source: str, translation: str, allow_missing_placeholde
problems.append(
f"placeholder mismatch: source={dict(source_placeholders)} translation={dict(translated_placeholders)}"
)
required_tags = extract_rich_tags(source)
translated_tags = extract_rich_tags(translation)
missing_tags = required_tags - translated_tags
if missing_tags:
problems.append(f"missing rich-text tags: {dict(missing_tags)}")
return problems
@@ -245,6 +292,10 @@ def extra_rich_tags(source: str, translation: str) -> collections.Counter[str]:
return extract_rich_tags(translation) - extract_rich_tags(source)
def missing_rich_tags(source: str, translation: str) -> collections.Counter[str]:
return extract_rich_tags(source) - extract_rich_tags(translation)
def message_to_batch_record(message: CatalogMessage) -> dict[str, object]:
return {
"record_type": "message",
+21
View File
@@ -17,9 +17,13 @@ from translation.ts_utils import ( # noqa: E402
SKIPPED_TYPES,
CatalogMessage,
extra_rich_tags,
extract_placeholders,
load_jsonl,
missing_rich_tags,
parse_catalog,
placeholder_counts_match,
placeholders_match,
unbalanced_rich_tags,
validate_translation,
)
@@ -109,11 +113,28 @@ def main() -> int:
if args.placeholders_only and not problem.startswith("placeholder mismatch"):
continue
errors.append(f"{label}: {problem}")
if placeholders_match(message.identity.source, value) and not placeholder_counts_match(
message.identity.source, value
):
source_placeholders = extract_placeholders(message.identity.source)
translated_placeholders = extract_placeholders(value)
warnings.append(
f"{label}: placeholder count mismatch: source={dict(source_placeholders)} "
f"translation={dict(translated_placeholders)}"
)
if not args.placeholders_only:
extras = extra_rich_tags(message.identity.source, value)
if extras:
output = errors if args.strict_extra_tags else warnings
output.append(f"{label}: additional rich-text tags: {dict(extras)}")
extras = missing_rich_tags(message.identity.source, value)
if extras:
output = errors if args.strict_extra_tags else warnings
output.append(f"{label}: missing rich-text tags: {dict(extras)}")
unbalanced = unbalanced_rich_tags(value)
if unbalanced:
output = errors if args.strict_extra_tags else warnings
output.append(f"{label}: unbalanced rich-text tags (opening, closing): {unbalanced}")
if (
message.identity.numerus
and any(value.strip() for value in values_for(message))
+1 -1
View File
@@ -728,7 +728,7 @@ bool AutoUpdaterDialog::extractUpdater(const std::string& zip_path, const std::s
tr("<h1>Inconsistent Application State</h1><h3>The update zip is missing the current executable:</h3><div "
"align=\"center\"><pre>%1</pre></div><p><strong>This is usually a result of manually renaming the "
"file.</strong> Continuing to install this update may result in a broken installation if the renamed "
"executable is used. The DuckStation executable should be named:<div "
"executable is used. The DuckStation executable should be named:</p><div "
"align=\"center\"><pre>%2</pre></div><p>Do you want to continue anyway?</p>")
.arg(QString::fromStdString(std::string(check_for_file)))
.arg(QStringLiteral(UPDATER_EXPECTED_EXECUTABLE)),
+2 -2
View File
@@ -149,7 +149,7 @@ CaptureSettingsWidget::CaptureSettingsWidget(SettingsWindow* dialog, QWidget* pa
"file will only contain audio."));
dialog->registerWidgetHelp(
m_ui.videoCaptureCodec, tr("Video Codec"), tr("Default"),
tr("Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.<b>"));
tr("Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.</b>"));
dialog->registerWidgetHelp(m_ui.videoCaptureBitrate, tr("Video Bitrate"),
tr("%1 kbps").arg(Settings::DEFAULT_MEDIA_CAPTURE_VIDEO_BITRATE),
tr("Sets the video bitrate to be used. Larger bitrate generally yields better video "
@@ -170,7 +170,7 @@ CaptureSettingsWidget::CaptureSettingsWidget(SettingsWindow* dialog, QWidget* pa
"file will only contain video."));
dialog->registerWidgetHelp(
m_ui.audioCaptureCodec, tr("Audio Codec"), tr("Default"),
tr("Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.<b>"));
tr("Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.</b>"));
dialog->registerWidgetHelp(m_ui.audioCaptureBitrate, tr("Audio Bitrate"),
tr("%1 kbps").arg(Settings::DEFAULT_MEDIA_CAPTURE_AUDIO_BITRATE),
tr("Sets the audio bitrate to be used."));
@@ -6667,8 +6667,8 @@ Haz clic en Reiniciar para restablecer el número de serie al que se ha encontra
<source>Rewind for %n frame(s), lasting %1 second(s) will require %2MB of RAM.</source>
<translatorcomment>Plural here is for &quot;frames&quot; only, not &quot;seconds&quot;.</translatorcomment>
<translation>
<numerusform>%n fotograma y %1 segundos de rebobinado requerirán %2 MB de RAM y %3 MB de VRAM.</numerusform>
<numerusform>%n fotogramas y %1 segundos de rebobinado requerirán %2 MB de RAM y %3 MB de VRAM.</numerusform>
<numerusform>%n fotograma y %1 segundos de rebobinado requerirán %2 MB de RAM.</numerusform>
<numerusform>%n fotogramas y %1 segundos de rebobinado requerirán %2 MB de RAM.</numerusform>
</translation>
</message>
<message>
@@ -9820,7 +9820,7 @@ Esta acción no puede deshacerse.</translation>
Error: {}
Please check your username and password, and try again.</source>
<translation>No se ha podido iniciar la sesión.
Error: %1
Error: {}
Comprueba tu nombre de usuario y contraseña y vuelve a intentarlo.</translation>
</message>
<message>
@@ -11273,7 +11273,7 @@ Comprueba tu nombre de usuario y contraseña y vuelve a intentarlo.</translation
<message>
<location filename="../../core/fullscreenui_strings.h" line="908"/>
<source>Yes, {} now and risk memory card corruption.</source>
<translation>, correr el riesgo de corromper la Memory Card.</translation>
<translation>, {} ahora y correr el riesgo de corromper la Memory Card.</translation>
</message>
<message>
<location filename="../../core/fullscreenui_strings.h" line="909"/>
@@ -15968,7 +15968,7 @@ Clic+Mayús: establecer varias asignaciones.</translation>
<location filename="../logwindow.cpp" line="64"/>
<source>Dropped %1 log messages, please use file or system console logging.
</source>
<translation>Se han omitido %d mensajes del registro, comprueba el registro por archivo o consola del sistema.</translation>
<translation>Se han omitido %1 mensajes del registro, comprueba el registro por archivo o consola del sistema.</translation>
</message>
</context>
<context>
@@ -4371,7 +4371,7 @@ Non puoi annullare quest&apos;azione.</translation>
<message>
<location filename="../coverdownloaddialog.ui" line="50"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Name component of the game&apos;s filename.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Serial of the game.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Example:&lt;/span&gt; https://www.example-not-a-real-domain.com/covers/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Nella casella qui sotto, specifica gli URL da cui scaricare le copertine, con un modello URL per riga. Le seguenti variabili sono disponibili:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Titolo del gioco.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Componente nome del nome file del gioco.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Seriale del gioco.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Esempio:&lt;/span&gt; https://www.esempio-non-un-dominio-reale.com/copertine/${seriale}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Nella casella qui sotto, specifica gli URL da cui scaricare le copertine, con un modello URL per riga. Le seguenti variabili sono disponibili:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Titolo del gioco.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Componente nome del nome file del gioco.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Seriale del gioco.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Esempio:&lt;/span&gt; https://www.esempio-non-un-dominio-reale.com/copertine/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../coverdownloaddialog.ui" line="63"/>
@@ -2550,8 +2550,8 @@ You can manually update DuckStation by re-downloading the latest release. Do you
</message>
<message>
<location filename="../autoupdaterdialog.cpp" line="728"/>
<source>&lt;h1&gt;Inconsistent Application State&lt;/h1&gt;&lt;h3&gt;The update zip is missing the current executable:&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;This is usually a result of manually renaming the file.&lt;/strong&gt; Continuing to install this update may result in a broken installation if the renamed executable is used. The DuckStation executable should be named:&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do you want to continue anyway?&lt;/p&gt;</source>
<translation>&lt;h1&gt;&lt;/h1&gt;&lt;h3&gt; ZIP :&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;使DuckStation :&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</translation>
<source>&lt;h1&gt;Inconsistent Application State&lt;/h1&gt;&lt;h3&gt;The update zip is missing the current executable:&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;This is usually a result of manually renaming the file.&lt;/strong&gt; Continuing to install this update may result in a broken installation if the renamed executable is used. The DuckStation executable should be named:&lt;/p&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do you want to continue anyway?&lt;/p&gt;</source>
<translation>&lt;h1&gt;&lt;/h1&gt;&lt;h3&gt; ZIP :&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;使DuckStation :&lt;/p&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</translation>
</message>
<message>
<location filename="../autoupdaterdialog.cpp" line="832"/>
@@ -3086,8 +3086,8 @@ Your dump may be corrupted, or the physical disc is scratched.</source>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="152"/>
<source>Selects which Video Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;b&gt;</source>
<translation>使&lt;b&gt;&lt;b&gt;</translation>
<source>Selects which Video Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;/b&gt;</source>
<translation>使&lt;b&gt;&lt;/b&gt;</translation>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="153"/>
@@ -3160,8 +3160,8 @@ Your dump may be corrupted, or the physical disc is scratched.</source>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="173"/>
<source>Selects which Audio Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;b&gt;</source>
<translation>使&lt;b&gt;&lt;b&gt;</translation>
<source>Selects which Audio Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;/b&gt;</source>
<translation>使&lt;b&gt;&lt;/b&gt;</translation>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="174"/>
@@ -6954,7 +6954,7 @@ You cannot undo this action.</source>
<message>
<location filename="../../core/fullscreen_ui.cpp" line="9516"/>
<source>BIOS to use when emulating {} consoles.</source>
<translation> .</translation>
<translation>{} .</translation>
</message>
<message>
<location filename="../../core/fullscreen_ui.cpp" line="9517"/>
@@ -9806,7 +9806,7 @@ Esta ação não pode ser desfeita.</translation>
<message>
<location filename="../../core/fullscreenui_strings.h" line="337"/>
<source>Example: https://www.example-not-a-real-domain.com/covers/${serial}.jpg</source>
<translation>Exemplo: https://www.exemplo-nao-eh-um-site.com/capas/${numeroserial}.jpg</translation>
<translation>Exemplo: https://www.exemplo-nao-eh-um-site.com/capas/${serial}.jpg</translation>
</message>
<message>
<location filename="../../core/fullscreenui_strings.h" line="340"/>
@@ -4233,7 +4233,7 @@ Esta ação não pode ser anulada.</translation>
<message>
<location filename="../../core/cpu_core.cpp" line="2074"/>
<source>No return instruction found after %u instructions for step-out at %08X.</source>
<translation>Nenhuma instrução de regresso encontrada após % u instruções para saída em %08X.</translation>
<translation>Nenhuma instrução de regresso encontrada após %u instruções para saída em %08X.</translation>
</message>
</context>
<context>
@@ -12238,7 +12238,7 @@ Tem a certeza que pretende continuar?</translation>
<message>
<location filename="../../core/memory_card.cpp" line="288"/>
<source>Memory card at &apos;%s&apos; could not be read, formatting.</source>
<translation>O cartão de memória em &apos;% s&apos; não pode ser lido, a formatar.</translation>
<translation>O cartão de memória em &apos;%s&apos; não pode ser lido, a formatar.</translation>
</message>
<message>
<location filename="../../core/memory_card.cpp" line="333"/>
@@ -440,7 +440,7 @@ Tek kullanımlık giriş kodu (access token) oluşturulma tarihi: %2.</translati
<message>
<location filename="../../core/achievements.cpp" line="1679"/>
<source>Your Value: {}</source>
<translation>Puanın: {}}</translation>
<translation>Puanın: {}</translation>
</message>
<message>
<location filename="../../core/achievements.cpp" line="1688"/>
@@ -4894,7 +4894,7 @@ Tüm atamalar ve ayarlar silinecek bunu geri alamazsınız.</translation>
<message>
<location filename="../coverdownloaddialog.ui" line="50"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Title of the game.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Name component of the game&apos;s filename.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Serial of the game.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Example:&lt;/span&gt; https://www.example-not-a-real-domain.com/covers/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Aşağıdaki kutuda kapak görseli indirilecek sitelerin linklerini yazabilirsiniz, Her satır için bir satır şablon link olacak şekilde. Şu değişkenler ayarlanabilir:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${İsim}:&lt;/span&gt; Oyunun ismi&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${Dosya İsmi}:&lt;/span&gt; Oyunun dosya ismi.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${seri no}:&lt;/span&gt; Oyunun seri numarası.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Örnek:&lt;/span&gt; https://www.ornek/kapaklar/${seri-no}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Aşağıdaki kutuda kapak görseli indirilecek sitelerin linklerini yazabilirsiniz, Her satır için bir satır şablon link olacak şekilde. Şu değişkenler ayarlanabilir:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${title}:&lt;/span&gt; Oyunun ismi&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${filetitle}:&lt;/span&gt; Oyunun dosya ismi.&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;${serial}:&lt;/span&gt; Oyunun seri numarası.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Örnek:&lt;/span&gt; https://www.ornek/kapaklar/${serial}.jpg&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../coverdownloaddialog.ui" line="69"/>
@@ -10485,12 +10485,12 @@ Yinede işleminize {0} devam etmek istiyor musunuz?</translation>
<message>
<location filename="../../core/fullscreen_ui.cpp" line="9831"/>
<source>{:%H:%M}</source>
<translation>{:%Saat:%Dakika}</translation>
<translation>{:%H:%M}</translation>
</message>
<message>
<location filename="../../core/fullscreen_ui.cpp" line="9832"/>
<source>{:%Y-%m-%d %H:%M:%S}</source>
<translation>{:%Yıl-%Ay-%Day %Saat:%Dakika:%Saniye}</translation>
<translation>{:%Y-%m-%d %H:%M:%S}</translation>
</message>
<message>
<location filename="../../core/fullscreen_ui.cpp" line="9833"/>
@@ -3374,7 +3374,7 @@ Your dump may be corrupted, or the physical disc is scratched.</source>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="151"/>
<source>Selects which Video Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;b&gt;</source>
<source>Selects which Video Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;/b&gt;</source>
<translation>Вибирає відеокодек для запису медіа. &lt;b&gt;Якщо не впевнені, залиште значення за замовчуванням.&lt;/b&gt;</translation>
</message>
<message>
@@ -3447,7 +3447,7 @@ Your dump may be corrupted, or the physical disc is scratched.</source>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="171"/>
<source>Selects which Audio Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;b&gt;</source>
<source>Selects which Audio Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;/b&gt;</source>
<translation>Вибирає аудіокодек для запису медіа. &lt;b&gt;Якщо не впевнені, залиште значення за замовчуванням.&lt;/b&gt;</translation>
</message>
<message>
@@ -2519,8 +2519,8 @@ You can manually update DuckStation by re-downloading the latest release. Do you
</message>
<message>
<location filename="../autoupdaterdialog.cpp" line="728"/>
<source>&lt;h1&gt;Inconsistent Application State&lt;/h1&gt;&lt;h3&gt;The update zip is missing the current executable:&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;This is usually a result of manually renaming the file.&lt;/strong&gt; Continuing to install this update may result in a broken installation if the renamed executable is used. The DuckStation executable should be named:&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do you want to continue anyway?&lt;/p&gt;</source>
<translation>&lt;h1&gt;&lt;/h1&gt;&lt;h3&gt;:&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;使DuckStation:&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</translation>
<source>&lt;h1&gt;Inconsistent Application State&lt;/h1&gt;&lt;h3&gt;The update zip is missing the current executable:&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;This is usually a result of manually renaming the file.&lt;/strong&gt; Continuing to install this update may result in a broken installation if the renamed executable is used. The DuckStation executable should be named:&lt;/p&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do you want to continue anyway?&lt;/p&gt;</source>
<translation>&lt;h1&gt;&lt;/h1&gt;&lt;h3&gt;:&lt;/h3&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%1&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;使DuckStation:&lt;/p&gt;&lt;div align=&quot;center&quot;&gt;&lt;pre&gt;%2&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</translation>
</message>
<message>
<location filename="../autoupdaterdialog.cpp" line="832"/>
@@ -3086,8 +3086,8 @@ Your dump may be corrupted, or the physical disc is scratched.</source>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="152"/>
<source>Selects which Video Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;b&gt;</source>
<translation>&lt;b&gt;&lt;b&gt;</translation>
<source>Selects which Video Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;/b&gt;</source>
<translation>&lt;b&gt;&lt;/b&gt;</translation>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="153"/>
@@ -3160,8 +3160,8 @@ Your dump may be corrupted, or the physical disc is scratched.</source>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="173"/>
<source>Selects which Audio Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;b&gt;</source>
<translation>&lt;b&gt;&lt;b&gt;</translation>
<source>Selects which Audio Codec to be used for media capture. &lt;b&gt;If unsure, leave it on default.&lt;/b&gt;</source>
<translation>&lt;b&gt;&lt;/b&gt;</translation>
</message>
<message>
<location filename="../capturesettingswidget.cpp" line="174"/>