"))
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 <strong>world</strong>",
+ "Use {0}, then use {0} again",
+ ).replace(
+ '',
+ "Usar {0}",
+ 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()
diff --git a/scripts/translation/ts_utils.py b/scripts/translation/ts_utils.py
index 941148bea..61945b193 100644
--- a/scripts/translation/ts_utils.py
+++ b/scripts/translation/ts_utils.py
@@ -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",
diff --git a/scripts/translation/validate_ts.py b/scripts/translation/validate_ts.py
index fe6a20257..e1ae8ab1d 100644
--- a/scripts/translation/validate_ts.py
+++ b/scripts/translation/validate_ts.py
@@ -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))
diff --git a/src/duckstation-qt/autoupdaterdialog.cpp b/src/duckstation-qt/autoupdaterdialog.cpp
index 59cda5f76..5d4ecd4bf 100644
--- a/src/duckstation-qt/autoupdaterdialog.cpp
+++ b/src/duckstation-qt/autoupdaterdialog.cpp
@@ -728,7 +728,7 @@ bool AutoUpdaterDialog::extractUpdater(const std::string& zip_path, const std::s
tr("
Inconsistent Application State
The update zip is missing the current executable:
%1
This is usually a result of manually renaming the "
"file. Continuing to install this update may result in a broken installation if the renamed "
- "executable is used. The DuckStation executable should be named:
%2
Do you want to continue anyway?
")
.arg(QString::fromStdString(std::string(check_for_file)))
.arg(QStringLiteral(UPDATER_EXPECTED_EXECUTABLE)),
diff --git a/src/duckstation-qt/capturesettingswidget.cpp b/src/duckstation-qt/capturesettingswidget.cpp
index d73bc4edd..b0a2ee20e 100644
--- a/src/duckstation-qt/capturesettingswidget.cpp
+++ b/src/duckstation-qt/capturesettingswidget.cpp
@@ -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. If unsure, leave it on default."));
+ tr("Selects which Video Codec to be used for media capture. If unsure, leave it on default."));
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. If unsure, leave it on default."));
+ tr("Selects which Audio Codec to be used for media capture. If unsure, leave it on default."));
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."));
diff --git a/src/duckstation-qt/translations/duckstation-qt_es-ES.ts b/src/duckstation-qt/translations/duckstation-qt_es-ES.ts
index 26b3a4d6d..3ce895717 100644
--- a/src/duckstation-qt/translations/duckstation-qt_es-ES.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_es-ES.ts
@@ -6667,8 +6667,8 @@ Haz clic en Reiniciar para restablecer el número de serie al que se ha encontra
Rewind for %n frame(s), lasting %1 second(s) will require %2MB of RAM.Plural here is for "frames" only, not "seconds".
- %n fotograma y %1 segundos de rebobinado requerirán %2 MB de RAM y %3 MB de VRAM.
- %n fotogramas y %1 segundos de rebobinado requerirán %2 MB de RAM y %3 MB de VRAM.
+ %n fotograma y %1 segundos de rebobinado requerirán %2 MB de RAM.
+ %n fotogramas y %1 segundos de rebobinado requerirán %2 MB de RAM.
@@ -9820,7 +9820,7 @@ Esta acción no puede deshacerse.
Error: {}
Please check your username and password, and try again.
No se ha podido iniciar la sesión.
-Error: %1
+Error: {}
Comprueba tu nombre de usuario y contraseña y vuelve a intentarlo.
@@ -11273,7 +11273,7 @@ Comprueba tu nombre de usuario y contraseña y vuelve a intentarlo.
Yes, {} now and risk memory card corruption.
- Sí, correr el riesgo de corromper la Memory Card.
+ Sí, {} ahora y correr el riesgo de corromper la Memory Card.
@@ -15968,7 +15968,7 @@ Clic+Mayús: establecer varias asignaciones.
Dropped %1 log messages, please use file or system console logging.
- Se han omitido %d mensajes del registro, comprueba el registro por archivo o consola del sistema.
+ Se han omitido %1 mensajes del registro, comprueba el registro por archivo o consola del sistema.
diff --git a/src/duckstation-qt/translations/duckstation-qt_it.ts b/src/duckstation-qt/translations/duckstation-qt_it.ts
index 7ba2052a7..e88db02a5 100644
--- a/src/duckstation-qt/translations/duckstation-qt_it.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_it.ts
@@ -4371,7 +4371,7 @@ Non puoi annullare quest'azione.
<html><head/><body><p>In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:</p><p><span style=" font-style:italic;">${title}:</span> Title of the game.<br/><span style=" font-style:italic;">${filetitle}:</span> Name component of the game's filename.<br/><span style=" font-style:italic;">${serial}:</span> Serial of the game.</p><p><span style=" font-weight:700;">Example:</span> https://www.example-not-a-real-domain.com/covers/${serial}.jpg</p></body></html>
- <html><head/><body><p>Nella casella qui sotto, specifica gli URL da cui scaricare le copertine, con un modello URL per riga. Le seguenti variabili sono disponibili:</p><p><span style=" font-style:italic;">${title}:</span> Titolo del gioco.<br/><span style=" font-style:italic;">${filetitle}:</span> Componente nome del nome file del gioco.<br/><span style=" font-style:italic;">${serial}:</span> Seriale del gioco.</p><p><span style=" font-weight:700;">Esempio:</span> https://www.esempio-non-un-dominio-reale.com/copertine/${seriale}.jpg</p></body></html>
+ <html><head/><body><p>Nella casella qui sotto, specifica gli URL da cui scaricare le copertine, con un modello URL per riga. Le seguenti variabili sono disponibili:</p><p><span style=" font-style:italic;">${title}:</span> Titolo del gioco.<br/><span style=" font-style:italic;">${filetitle}:</span> Componente nome del nome file del gioco.<br/><span style=" font-style:italic;">${serial}:</span> Seriale del gioco.</p><p><span style=" font-weight:700;">Esempio:</span> https://www.esempio-non-un-dominio-reale.com/copertine/${serial}.jpg</p></body></html>
diff --git a/src/duckstation-qt/translations/duckstation-qt_ja.ts b/src/duckstation-qt/translations/duckstation-qt_ja.ts
index 86d9f3292..8365b7096 100644
--- a/src/duckstation-qt/translations/duckstation-qt_ja.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_ja.ts
@@ -2550,8 +2550,8 @@ You can manually update DuckStation by re-downloading the latest release. Do you
- <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 align="center"><pre>%2</pre></div><p>Do you want to continue anyway?</p>
- <h1>アプリケーションの状態が一致しません</h1><h3>更新用 ZIP に現在の実行ファイルが含まれていません:</h3><div align="center"><pre>%1</pre></div><p><strong>通常、これはファイル名を手動で変更したことが原因です。</strong>名前を変更した実行ファイルを使用している場合、この更新を続行するとインストールが破損する可能性があります。DuckStation の実行ファイル名は次のとおりである必要があります:<div align="center"><pre>%2</pre></div><p>それでも続行しますか?</p>
+ <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:</p><div align="center"><pre>%2</pre></div><p>Do you want to continue anyway?</p>
+ <h1>アプリケーションの状態が一致しません</h1><h3>更新用 ZIP に現在の実行ファイルが含まれていません:</h3><div align="center"><pre>%1</pre></div><p><strong>通常、これはファイル名を手動で変更したことが原因です。</strong>名前を変更した実行ファイルを使用している場合、この更新を続行するとインストールが破損する可能性があります。DuckStation の実行ファイル名は次のとおりである必要があります:</p><div align="center"><pre>%2</pre></div><p>それでも続行しますか?</p>
@@ -3086,8 +3086,8 @@ Your dump may be corrupted, or the physical disc is scratched.
- Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.<b>
- メディアキャプチャに使用する映像コーデックを選択します。<b>不明な場合は、デフォルトのままにしてください。<b>
+ Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.</b>
+ メディアキャプチャに使用する映像コーデックを選択します。<b>不明な場合は、デフォルトのままにしてください。</b>
@@ -3160,8 +3160,8 @@ Your dump may be corrupted, or the physical disc is scratched.
- Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.<b>
- メディアキャプチャに使用する音声コーデックを選択します。<b>不明な場合は、デフォルトのままにしてください。<b>
+ Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.</b>
+ メディアキャプチャに使用する音声コーデックを選択します。<b>不明な場合は、デフォルトのままにしてください。</b>
diff --git a/src/duckstation-qt/translations/duckstation-qt_ko.ts b/src/duckstation-qt/translations/duckstation-qt_ko.ts
index 6f955db8e..de2e9ffc7 100644
--- a/src/duckstation-qt/translations/duckstation-qt_ko.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_ko.ts
@@ -6954,7 +6954,7 @@ You cannot undo this action.
BIOS to use when emulating {} consoles.
- 콘솔을 에뮬레이트할 때 사용할 바이오스입니다.
+ {} 콘솔을 에뮬레이트할 때 사용할 바이오스입니다.
diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-BR.ts b/src/duckstation-qt/translations/duckstation-qt_pt-BR.ts
index aa003554b..84138740a 100644
--- a/src/duckstation-qt/translations/duckstation-qt_pt-BR.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_pt-BR.ts
@@ -9806,7 +9806,7 @@ Esta ação não pode ser desfeita.
Example: https://www.example-not-a-real-domain.com/covers/${serial}.jpg
- Exemplo: https://www.exemplo-nao-eh-um-site.com/capas/${numeroserial}.jpg
+ Exemplo: https://www.exemplo-nao-eh-um-site.com/capas/${serial}.jpg
diff --git a/src/duckstation-qt/translations/duckstation-qt_pt-PT.ts b/src/duckstation-qt/translations/duckstation-qt_pt-PT.ts
index 515ab602e..fa63df8f3 100644
--- a/src/duckstation-qt/translations/duckstation-qt_pt-PT.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_pt-PT.ts
@@ -4233,7 +4233,7 @@ Esta ação não pode ser anulada.
No return instruction found after %u instructions for step-out at %08X.
- Nenhuma instrução de regresso encontrada após % u instruções para saída em %08X.
+ Nenhuma instrução de regresso encontrada após %u instruções para saída em %08X.
@@ -12238,7 +12238,7 @@ Tem a certeza que pretende continuar?
Memory card at '%s' could not be read, formatting.
- O cartão de memória em '% s' não pode ser lido, a formatar.
+ O cartão de memória em '%s' não pode ser lido, a formatar.
diff --git a/src/duckstation-qt/translations/duckstation-qt_tr.ts b/src/duckstation-qt/translations/duckstation-qt_tr.ts
index 0d5746712..e0cb876f1 100644
--- a/src/duckstation-qt/translations/duckstation-qt_tr.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_tr.ts
@@ -440,7 +440,7 @@ Tek kullanımlık giriş kodu (access token) oluşturulma tarihi: %2.
Your Value: {}
- Puanın: {}}
+ Puanın: {}
@@ -4894,7 +4894,7 @@ Tüm atamalar ve ayarlar silinecek bunu geri alamazsınız.
<html><head/><body><p>In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:</p><p><span style=" font-style:italic;">${title}:</span> Title of the game.<br/><span style=" font-style:italic;">${filetitle}:</span> Name component of the game's filename.<br/><span style=" font-style:italic;">${serial}:</span> Serial of the game.</p><p><span style=" font-weight:700;">Example:</span> https://www.example-not-a-real-domain.com/covers/${serial}.jpg</p></body></html>
- <html><head/><body><p>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:</p><p><span style=" font-style:italic;">${İsim}:</span> Oyunun ismi<br/><span style=" font-style:italic;">${Dosya İsmi}:</span> Oyunun dosya ismi.<br/><span style=" font-style:italic;">${seri no}:</span> Oyunun seri numarası.</p><p><span style=" font-weight:700;">Örnek:</span> https://www.ornek/kapaklar/${seri-no}.jpg</p></body></html>
+ <html><head/><body><p>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:</p><p><span style=" font-style:italic;">${title}:</span> Oyunun ismi<br/><span style=" font-style:italic;">${filetitle}:</span> Oyunun dosya ismi.<br/><span style=" font-style:italic;">${serial}:</span> Oyunun seri numarası.</p><p><span style=" font-weight:700;">Örnek:</span> https://www.ornek/kapaklar/${serial}.jpg</p></body></html>
@@ -10485,12 +10485,12 @@ Yinede işleminize {0} devam etmek istiyor musunuz?
{:%H:%M}
- {:%Saat:%Dakika}
+ {:%H:%M}{:%Y-%m-%d %H:%M:%S}
- {:%Yıl-%Ay-%Day %Saat:%Dakika:%Saniye}
+ {:%Y-%m-%d %H:%M:%S}
diff --git a/src/duckstation-qt/translations/duckstation-qt_uk.ts b/src/duckstation-qt/translations/duckstation-qt_uk.ts
index 5bee09d6c..f7e7f992d 100644
--- a/src/duckstation-qt/translations/duckstation-qt_uk.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_uk.ts
@@ -3374,7 +3374,7 @@ Your dump may be corrupted, or the physical disc is scratched.
- Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.<b>
+ Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.</b>Вибирає відеокодек для запису медіа. <b>Якщо не впевнені, залиште значення за замовчуванням.</b>
@@ -3447,7 +3447,7 @@ Your dump may be corrupted, or the physical disc is scratched.
- Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.<b>
+ Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.</b>Вибирає аудіокодек для запису медіа. <b>Якщо не впевнені, залиште значення за замовчуванням.</b>
diff --git a/src/duckstation-qt/translations/duckstation-qt_zh-CN.ts b/src/duckstation-qt/translations/duckstation-qt_zh-CN.ts
index e7d864c55..6f5d4efb2 100644
--- a/src/duckstation-qt/translations/duckstation-qt_zh-CN.ts
+++ b/src/duckstation-qt/translations/duckstation-qt_zh-CN.ts
@@ -2519,8 +2519,8 @@ You can manually update DuckStation by re-downloading the latest release. Do you
- <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 align="center"><pre>%2</pre></div><p>Do you want to continue anyway?</p>
- <h1>应用程序状态不一致</h1><h3>更新压缩包缺少当前可执行文件:</h3><div align="center"><pre>%1</pre></div><p><strong>这通常是手动重命名文件造成的。</strong>如果继续安装此更新,使用重命名后的可执行文件可能会导致安装损坏。DuckStation可执行文件应命名为:<div align="center"><pre>%2</pre></div><p>您仍然想要继续吗?</p>
+ <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:</p><div align="center"><pre>%2</pre></div><p>Do you want to continue anyway?</p>
+ <h1>应用程序状态不一致</h1><h3>更新压缩包缺少当前可执行文件:</h3><div align="center"><pre>%1</pre></div><p><strong>这通常是手动重命名文件造成的。</strong>如果继续安装此更新,使用重命名后的可执行文件可能会导致安装损坏。DuckStation可执行文件应命名为:</p><div align="center"><pre>%2</pre></div><p>您仍然想要继续吗?</p>
@@ -3086,8 +3086,8 @@ Your dump may be corrupted, or the physical disc is scratched.
- Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.<b>
- 选择用于媒体捕获的视频编解码器。<b>如果不确定,请保持默认。<b>
+ Selects which Video Codec to be used for media capture. <b>If unsure, leave it on default.</b>
+ 选择用于媒体捕获的视频编解码器。<b>如果不确定,请保持默认。</b>
@@ -3160,8 +3160,8 @@ Your dump may be corrupted, or the physical disc is scratched.
- Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.<b>
- 选择用于媒体捕获的音频编解码器。<b>如果不确定,请保持默认。<b>
+ Selects which Audio Codec to be used for media capture. <b>If unsure, leave it on default.</b>
+ 选择用于媒体捕获的音频编解码器。<b>如果不确定,请保持默认。</b>