diff --git a/scripts/translation/tests/test_translation_tools.py b/scripts/translation/tests/test_translation_tools.py index f73c76ba0..a4196c49e 100644 --- a/scripts/translation/tests/test_translation_tools.py +++ b/scripts/translation/tests/test_translation_tools.py @@ -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("")) self.assertEqual({"strong": 1, "/strong": 1}, dict(extract_rich_tags("Text"))) - self.assertTrue(validate_translation("Text", "テキスト")) + self.assertFalse(validate_translation("Text", "テキスト")) + + def test_rich_text_tag_pairs_are_balanced(self) -> None: + self.assertFalse(unbalanced_rich_tags("

")) + self.assertFalse(unbalanced_rich_tags("Text")) + self.assertEqual({"p": (2, 1), "strong": (1, 0)}, unbalanced_rich_tags("

Text

")) 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>