Merge pull request #120 from m2k7m/1.7

Update Arabic translations
This commit is contained in:
Drakmor authored and GitHub committed 2026-10-03 14:34:47 +03:00
commit d7e35e6ce9
4 files changed
+1110 -31

No files matched your search

+29 -29
View File
@@ -7,81 +7,81 @@ static const char *const g_ar_sa[SM_L10N_COUNT] = {
[SM_L10N_STARTUP_WEB] =
"ShadowMount+ v%s\n© Drakmor والمجتمع\nالتحكم: http://%s:%u",
/* English: ShadowMount+: recursive_scan=1 is deprecated, using scan_depth=2. */
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "شادوماونت+: لم يعد الفحص التكراري مدعوماً، وسيتم استخدام عمق الفحص ٢.",
[SM_L10N_RECURSIVE_SCAN_DEPRECATED] = "ShadowMount+: الخيار recursive_scan=1 مهجور، وسيُستخدم scan_depth=2 بدلًا منه.",
/* English: ShadowMount+: scan depth %u enabled. */
[SM_L10N_SCAN_DEPTH_ENABLED] = "شادوماونت+: تم تفعيل عمق الفحص %u.",
[SM_L10N_SCAN_DEPTH_ENABLED] = "ShadowMount+: تم تمكين عمق المسح %u.",
/* English: Backport overlay failed: %s\n%s\n0x%08X */
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "فشل تركيب طبقة التوافق مع الإصدارات الأقدم: %s\n%s\n0x%08X",
[SM_L10N_BACKPORT_OVERLAY_FAILED] = "فشل تطبيق طبقة الدعم الرجعي: %s\n%s\n0x%08X",
/* English: Backport applied to game: %s */
[SM_L10N_GAME_BACKPORTED] = "طُبّق دعم الإصدار الأقدم على اللعبة: %s",
[SM_L10N_GAME_BACKPORTED] = "طُبّق الدعم الرجعي على اللعبة: %s",
/* English: Backport applied to game: %s\nEmulators updated */
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "طُبّق دعم الإصدار الأقدم على اللعبة: %s\nتم تحديث المحاكيات",
[SM_L10N_GAME_BACKPORTED_EMULATORS_UPDATED] = "طُبّق الدعم الرجعي على اللعبة: %s\nتم تحديث المحاكيات",
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nSaved image_sector override: %u.\nTry mounting again. */
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "تم رفض تحميل الصورة:\n%s\nحجم الكتلة صغير جداً (%llu < %u).\nتم حفظ قيمة حجم القطاع الجديدة: %u.\nحاول التحميل مرة أخرى.",
[SM_L10N_IMAGE_REJECTED_CLUSTER_AUTOTUNED] = "تم رفض تركيب صورة القرص:\n%s\nحجم العنقود صغير جدًا (%llu < %u).\nتم حفظ تجاوز قطاع صورة القرص: %u.\nحاول التركيب مرة أخرى.",
/* English: Image mount rejected:\n%s\nCluster size is too small (%llu < %u).\nUse a larger cluster size in the image or lower sector size in config. */
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "تم رفض تحميل الصورة:\n%s\nحجم الكتلة صغير جداً (%llu < %u).\nاستخدم حجماً أكبر للكتلة في الصورة أو حجماً أصغر للقطاع في الإعدادات.",
[SM_L10N_IMAGE_REJECTED_CLUSTER_CONFIG] = "تم رفض تركيب صورة القرص:\n%s\nحجم العنقود صغير جدًا (%llu < %u).\nاستخدم حجمًا أكبر للعنقود في صورة القرص أو حجمًا أصغر للقطاع في الإعدادات.",
/* English: Register failed: %s (%s)\nAppInstallTitleDir unavailable */
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "فشل التسجيل: %s (%s)\nمجلد تثبيت التطبيقات غير متاح",
[SM_L10N_REGISTER_FAILED_UNAVAILABLE] = "فشل التسجيل: %s (%s)\nAppInstallTitleDir غير متاح",
/* English: Register failed: %s (%s)\nTitleDir bridge unavailable */
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "تعذر التسجيل: %s (%s)\nجسر TitleDir غير متاح",
[SM_L10N_REGISTER_BRIDGE_UNAVAILABLE] = "فشل التسجيل: %s (%s)\nجسر TitleDir غير متاح",
/* English: Register failed: %s (%s)\ncode=0x%08X */
[SM_L10N_REGISTER_FAILED_CODE] = "فشل التسجيل: %s (%s)\nالرمز=0x%08X",
/* English: Installing: %s (%s)... */
[SM_L10N_INSTALLING] = "جارٍ التثبيت: %s (%s)...",
/* English: KStuff active again */
[SM_L10N_KSTUFF_ACTIVE] = "أصبح KStuff نشطًا من جديد",
[SM_L10N_KSTUFF_ACTIVE] = "عاد KStuff إلى العمل",
/* English: KStuff paused while the game is running */
[SM_L10N_KSTUFF_PAUSED] = "أُوقف KStuff مؤقتًا أثناء تشغيل اللعبة",
[SM_L10N_KSTUFF_PAUSED] = "تم إيقاف KStuff مؤقتًا أثناء تشغيل اللعبة",
/* English: Installed: %s */
[SM_L10N_INSTALLED] = "تم التثبيت: %s",
/* English: Unknown */
[SM_L10N_UNKNOWN] = "غير معروف",
/* English: Image mount failed: 0x%08X (%s)\n%s */
[SM_L10N_IMAGE_MOUNT_FAILED] = "فشل تحميل الصورة: 0x%08X (%s)\n%s",
[SM_L10N_IMAGE_MOUNT_FAILED] = "فشل تركيب صورة القرص: 0x%08X (%s)\n%s",
/* English: Filesystem cluster size (%llu) is smaller than device sector size (%u) for %s */
[SM_L10N_FS_CLUSTER_SMALL] = "حجم كتلة نظام الملفات (%llu) أصغر من حجم قطاع الجهاز (%u) لـ %s",
[SM_L10N_FS_CLUSTER_SMALL] = "حجم عنقود نظام الملفات (%llu) أصغر من حجم قطاع الجهاز (%u) للجهاز %s",
/* English: Mounted image is unreadable or damaged */
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "الصورة المحمّلة غير قابلة للقراءة أو تالفة",
[SM_L10N_IMAGE_UNREADABLE_DAMAGED] = "صورة القرص المركّبة غير قابلة للقراءة أو تالفة",
/* English: Image cache full (%u entries), cannot track mount %s */
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "ذاكرة الصور المؤقتة ممتلئة (%u إدخالًا)، ولا يمكن تتبع التركيب %s",
[SM_L10N_IMAGE_CACHE_FULL_TRACK] = "ذاكرة صور القرص المؤقتة ممتلئة (%u إدخالًا)، لا يمكن تتبع التركيب %s",
/* English: Install failed: %s (%s)\nTimed out after 5 minutes */
[SM_L10N_INSTALL_FAILED_TIMEOUT] = "فشل التثبيت: %s (%s)\nانتهت المهلة بعد 5 دقائق",
/* English: Batch install queued (%d): */
[SM_L10N_BATCH_INSTALL_QUEUED] = "تمت إضافة التثبيت الجماعي إلى قائمة الانتظار (%d):",
[SM_L10N_BATCH_INSTALL_QUEUED] = "تم وضع التثبيت الجماعي في قائمة الانتظار (%d):",
/* English: \n... and %d more */
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... و %d أخرى",
[SM_L10N_BATCH_INSTALL_AND_MORE] = "\n... و %d إضافية",
/* English: Batch install failed for %d app(s).\ncode=0x%08X */
[SM_L10N_BATCH_INSTALL_FAILED] = "فشل التثبيت الجماعي لـ %d تطبيق.\nالرمز=0x%08X",
[SM_L10N_BATCH_INSTALL_FAILED] = "فشل التثبيت الجماعي لـ %d من التطبيقات.\nالرمز=0x%08X",
/* English: %s crashed before KStuff was paused. KStuff is not the cause. */
[SM_L10N_CRASH_BEFORE_KSTUFF] = "تعطّل التطبيق %s قبل إيقاف KStuff مؤقتًا. لم يكن KStuff هو السبب.",
/* English: %s crashed before KStuff pause:\n%s */
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "تعطل %s قبل إيقاف KStuff مؤقتًا:\n%s",
[SM_L10N_CRASH_BEFORE_KSTUFF_FATAL] = "تعطّل التطبيق %s قبل إيقاف KStuff مؤقتًا:\n%s",
/* English: %s crashed after KStuff was paused. Automatic tuning skipped. */
[SM_L10N_CRASH_AFTER_KSTUFF_SKIPPED] = "تعطّل التطبيق %s بعد إيقاف KStuff مؤقتًا. تم تخطي الضبط التلقائي.",
/* English: %s crashed after KStuff pause:\n%s\nAutotune skipped. */
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "تعطل %s بعد إيقاف KStuff مؤقتًا:\n%s\nتم تخطي الضبط التلقائي.",
[SM_L10N_CRASH_AFTER_KSTUFF_FATAL_SKIPPED] = "تعطّل التطبيق %s بعد إيقاف KStuff مؤقتًا:\n%s\nتم تخطي الضبط التلقائي.",
/* English: Can't load module after KStuff pause */
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "تعذر تحميل الوحدة بعد إيقاف كايستاف مؤقتاً",
[SM_L10N_RTLD_AFTER_KSTUFF_FALLBACK] = "تعذر تحميل الوحدة بعد إيقاف KStuff مؤقتًا",
/* English: can't load module %.*s after KStuff pause */
[SM_L10N_RTLD_MODULE_AFTER_KSTUFF] = "تعذر تحميل الوحدة %.*s بعد إيقاف KStuff مؤقتًا",
/* English: %s: %s. Delay increased to %us.\nLaunch the game again. */
[SM_L10N_DELAY_INCREASED_RELAUNCH] = "%s: %s. تمت زيادة التأخير إلى %u ثانية.\nشغّل اللعبة مرة أخرى.",
/* English: Crash detected after KStuff pause: pause delay for %s increased to %us. Launch the game again. */
[SM_L10N_CRASH_DELAY_INCREASED] = "تم اكتشاف تعطل بعد إيقاف كايستاف مؤقتاً: تمت زيادة مدة الإيقاف الخاصة بـ %s إلى %u ثانية. شغّل اللعبة مرة أخرى.",
[SM_L10N_CRASH_DELAY_INCREASED] = "تم اكتشاف تعطّل بعد إيقاف KStuff مؤقتًا: تمت زيادة تأخير الإيقاف للتطبيق %s إلى %u ثانية. شغّل اللعبة مرة أخرى.",
/* English: %s crashed after KStuff pause:\n%s\nDelay increased to %us.\nLaunch the game again. */
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "تعطل %s بعد إيقاف KStuff مؤقتًا:\n%s\nتمت زيادة التأخير إلى %u ث.\nشغّل اللعبة مجددًا.",
[SM_L10N_CRASH_FATAL_DELAY_INCREASED] = "تعطّل التطبيق %s بعد إيقاف KStuff مؤقتًا:\n%s\nتمت زيادة التأخير إلى %u ثانية.\nشغّل اللعبة مرة أخرى.",
/* English: Crash detected after KStuff pause: %s. Failed to update autotune delay. */
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "تم اكتشاف تعطل بعد إيقاف كايستاف مؤقتاً: %s. تعذر تحديث مدة التحسين التلقائي.",
[SM_L10N_CRASH_DELAY_UPDATE_FAILED] = "تم اكتشاف تعطّل بعد إيقاف KStuff مؤقتًا: %s. تعذر تحديث تأخير الضبط التلقائي.",
/* English: %s crashed after KStuff pause:\n%s\nFailed to update autotune delay. */
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "تعطل %s بعد إيقاف KStuff مؤقتًا:\n%s\nتعذر تحديث تأخير الضبط التلقائي.",
[SM_L10N_CRASH_FATAL_DELAY_UPDATE_FAILED] = "تعطّل التطبيق %s بعد إيقاف KStuff مؤقتًا:\n%s\nتعذر تحديث تأخير الضبط التلقائي.",
/* English: Missing/invalid param.json:\n%s */
[SM_L10N_MISSING_PARAM] = "ملف param.json مفقود أو غير صالح:\n%s",
/* English: Found %d new games. Processing... */
[SM_L10N_FOUND_NEW_GAMES] = "عُثر على %d ألعاب جديدة. جارٍ معالجتها...",
[SM_L10N_FOUND_NEW_GAMES] = "عُثر على %d من الألعاب الجديدة. جارٍ معالجتها...",
/* English: Library synchronized.\nFound %d games. */
[SM_L10N_LIBRARY_SYNCED] = "تمت مزامنة المكتبة.\nتم العثور على %d ألعاب.",
[SM_L10N_LIBRARY_SYNCED] = "تمت مزامنة المكتبة.\nعُثر على %d من الألعاب.",
/* English: ShadowMount+: config reloaded. */
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: أُعيد تحميل الإعدادات.",
[SM_L10N_CONFIG_RELOADED] = "ShadowMount+: تمت إعادة تحميل الإعدادات.",
/* English: Duplicate %s ignored:\n%s\nexisting: %s */
[SM_L10N_DUPLICATE_IGNORED] = "تم تجاهل %s المكرر:\n%s\nالموجود مسبقًا: %s",
/* English: AMPR emulator updated */
@@ -91,7 +91,7 @@ static const char *const g_ar_sa[SM_L10N_COUNT] = {
/* English: Target temperature %u °C → %u °C */
[SM_L10N_TEMPERATURE_TRANSITION] = "درجة الحرارة المستهدفة %u °C → %u °C",
/* English: External drive %s connected\nFree %llu.%llu GB of %llu.%llu (Block size %llu KB)\nGames on drive: %d%s */
[SM_L10N_USB_CONNECTED_SCANNING] = "تم توصيل محرك الأقراص الخارجي %s.\nالمتاح %llu.%llu من %llu.%llu غيغابايت (حجم الكتلة %llu كيلوبايت)\nالألعاب على القرص: %d%s",
[SM_L10N_USB_CONNECTED_SCANNING] = "تم توصيل محرك الأقراص الخارجي %s\nالمساحة المتاحة %llu.%llu من %llu.%llu غيغابايت (حجم الكتلة %llu كيلوبايت)\nالألعاب على القرص: %d%s",
/* English: \nFor maximum performance, use a cluster size of no more than 64 KB */
[SM_L10N_USB_CLUSTER_RECOMMENDATION] = "\nللحصول على أقصى أداء، استخدم حجم عنقود لا يزيد عن 64 كيلوبايت",
/* English: External drive %s disconnected */
+667
View File
@@ -0,0 +1,667 @@
#!/usr/bin/env python3
"""Export one web UI translation catalog from web/index.html to JSONC.
The web UI stores its translations in two shapes: ``en-US`` and ``ru-RU`` are
key/value objects inside ``translations``, while every other locale is a
positional array inside ``translationRows`` indexed by ``translationKeys``.
Both shapes are normalized into a single key/value JSONC file so translators
work with readable keys instead of array offsets, and each value is preceded by
a ``// English:`` comment taken from the ``en-US`` catalog.
Usage:
python tools/extract_web_l10n.py ar-SA
python tools/extract_web_l10n.py ru-RU --byte-units
python tools/extract_web_l10n.py --all
Run tools/merge_web_l10n.py to write the edited JSONC back into web/index.html.
"""
from __future__ import annotations
import argparse
import dataclasses
import itertools
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_HTML = ROOT / "web" / "index.html"
DEFAULT_OUT_DIR = ROOT / "web" / "l10n"
BASE_LOCALE = "en-US"
BYTE_UNITS_SUFFIX = ".byte-units.jsonc"
BYTE_UNIT_KEYS = ("bytes", "kibibytes", "mebibytes", "gibibytes", "tebibytes")
BYTE_UNIT_BASE_VALUES = ("B", "KiB", "MiB", "GiB", "TiB")
LOCALE_RE = re.compile(r"^[a-z]{2}-[A-Z]{2}$")
STRING_RE = re.compile(r'"(?:\\.|[^"\\])*"')
OPTION_RE = re.compile(r'<option value="([a-z]{2}-[A-Z]{2})">([^<]*)</option>')
PAIRS = {"{": "}", "[": "]"}
IDENTIFIER_START = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$")
IDENTIFIER_BODY = IDENTIFIER_START | set("0123456789")
SIMPLE_ESCAPES = {
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
"v": "\v",
"0": "\0",
}
class L10nError(Exception):
"""Raised when web/index.html cannot be parsed."""
@dataclasses.dataclass
class JsEntry:
"""One ``key: value`` pair inside a JavaScript object literal."""
key: str
key_start: int
start: int
end: int
indent: str
@dataclasses.dataclass
class JsBlock:
"""A locale entry whose value is itself an object or array literal."""
name: str
start: int
end: int
indent: str
entries: list[JsEntry]
@dataclasses.dataclass
class Document:
"""The parsed localization state of web/index.html."""
text: str
order: list[str]
object_locales: dict[str, list[tuple[str, str]]]
object_indents: dict[str, tuple[str, str]]
row_locales: dict[str, list[str]]
row_indents: dict[str, str]
byte_units: dict[str, list[str]]
byte_unit_indents: dict[str, str]
row_separator: str
unit_separator: str
labels: dict[str, str]
spans: dict[str, tuple[int, int]]
@property
def locales(self) -> list[str]:
return [*self.object_locales, *self.row_locales]
@property
def orphan_keys(self) -> list[str]:
"""Keys present in the object-form locales but absent from translationKeys."""
base = self.object_locales.get(BASE_LOCALE, [])
return [key for key, _ in base if key not in self.order]
def read_source(path: Path) -> tuple[str, str]:
"""Return the file contents normalized to LF plus its original line ending."""
try:
raw = path.read_bytes()
except OSError as exc:
raise L10nError(f"cannot read {path}: {exc}") from exc
try:
if raw.startswith(b"\xef\xbb\xbf"):
raw = raw[3:]
text = raw.decode("utf-8")
except UnicodeDecodeError as exc:
raise L10nError(f"{path} is not valid UTF-8: {exc}") from exc
newline = "\r\n" if "\r\n" in text else "\n"
return text.replace("\r\n", "\n"), newline
def write_text(path: Path, text: str, newline: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(text.replace("\n", newline).encode("utf-8"))
def _skip_string(text: str, index: int) -> int:
"""Return the offset just past the string literal starting at ``index``."""
quote = text[index]
cursor = index + 1
limit = len(text)
while cursor < limit:
char = text[cursor]
if char == "\\":
cursor += 2
continue
if char == quote:
return cursor + 1
cursor += 1
raise L10nError(f"unterminated {quote} string at offset {index}")
def _skip_comment(text: str, index: int) -> int:
if text[index : index + 2] == "//":
end = text.find("\n", index)
return len(text) if end < 0 else end + 1
end = text.find("*/", index + 2)
if end < 0:
raise L10nError(f"unterminated block comment at offset {index}")
return end + 2
def _skip_whitespace(text: str, index: int) -> int:
cursor = index
limit = len(text)
while cursor < limit:
if text[cursor].isspace():
cursor += 1
elif text[cursor : cursor + 2] in ("//", "/*"):
cursor = _skip_comment(text, cursor)
else:
break
return cursor
def find_block(text: str, start: int) -> int:
"""Return the offset of the delimiter closing the block at ``start``."""
if text[start] not in PAIRS:
raise L10nError(f"expected a block at offset {start}, found {text[start]!r}")
stack = [PAIRS[text[start]]]
cursor = start + 1
limit = len(text)
while cursor < limit and stack:
char = text[cursor]
if char in "\"'`":
cursor = _skip_string(text, cursor)
continue
if char == "/" and text[cursor : cursor + 2] in ("//", "/*"):
cursor = _skip_comment(text, cursor)
continue
if char in PAIRS:
stack.append(PAIRS[char])
elif char in ("}", "]"):
if char != stack[-1]:
raise L10nError(f"mismatched {char!r} at offset {cursor}")
stack.pop()
cursor += 1
if stack:
raise L10nError(f"unterminated block at offset {start}")
return cursor - 1
def decode_js_string(token: str) -> str:
"""Decode a JavaScript string literal into a Python string."""
quote = token[0]
if quote == '"':
try:
return json.loads(token)
except json.JSONDecodeError as exc:
raise L10nError(f"invalid string literal: {exc}") from exc
if quote not in ("'", "`"):
raise L10nError(f"expected a string literal, found {token[:32]!r}")
body = token[1:-1]
out: list[str] = []
cursor = 0
while cursor < len(body):
char = body[cursor]
if char != "\\":
out.append(char)
cursor += 1
continue
cursor += 1
if cursor >= len(body):
raise L10nError("dangling escape in string literal")
escape = body[cursor]
if escape == "u":
if body[cursor + 1 : cursor + 2] == "{":
end = body.index("}", cursor)
out.append(chr(int(body[cursor + 2 : end], 16)))
cursor = end + 1
else:
out.append(chr(int(body[cursor + 1 : cursor + 5], 16)))
cursor += 5
continue
if escape == "x":
out.append(chr(int(body[cursor + 1 : cursor + 3], 16)))
cursor += 3
continue
out.append(SIMPLE_ESCAPES.get(escape, escape))
cursor += 1
return "".join(out)
def string_separator(text: str, start: int, end: int) -> str:
"""Return the separator used between array items, "," or ", "."""
matches = list(STRING_RE.finditer(text, start, end))
for previous, current in itertools.pairwise(matches):
gap = text[previous.end() : current.start()]
if gap in (",", ", "):
return gap
return ","
def js_string(value: str) -> str:
"""Encode a Python string as a double-quoted JavaScript string literal."""
return json.dumps(value, ensure_ascii=False)
def line_indent(text: str, index: int) -> str:
"""Return the whitespace between the start of the line and ``index``."""
start = text.rfind("\n", 0, index) + 1
prefix = text[start:index]
return prefix if prefix.strip() == "" else ""
def closing_indent(text: str, close: int) -> str:
"""Return the indentation of the line holding the closing delimiter."""
start = text.rfind("\n", 0, close) + 1
prefix = text[start:close]
if prefix.strip():
raise L10nError(f"closing delimiter at offset {close} is not on its own line")
return prefix
def value_end(text: str, index: int) -> int:
"""Return the offset just past the value starting at ``index``."""
char = text[index]
if char in PAIRS:
return find_block(text, index) + 1
if char in "\"'`":
return _skip_string(text, index)
cursor = index
while cursor < len(text) and text[cursor] not in ",}]:\n \t":
cursor += 1
return cursor
def object_entries(text: str, open_index: int) -> tuple[list[JsEntry], int]:
"""Parse the top-level ``key: value`` pairs of an object literal."""
close = find_block(text, open_index)
entries: list[JsEntry] = []
cursor = open_index + 1
while cursor < close:
char = text[cursor]
if char.isspace() or char == ",":
cursor += 1
continue
if char == "/" and text[cursor : cursor + 2] in ("//", "/*"):
cursor = _skip_comment(text, cursor)
continue
if char in "\"'`" or char in IDENTIFIER_START:
key_start = cursor
if char in "\"'`":
key_end = _skip_string(text, cursor)
key = decode_js_string(text[key_start:key_end])
cursor = key_end
else:
cursor += 1
while cursor < close and text[cursor] in IDENTIFIER_BODY:
cursor += 1
key = text[key_start:cursor]
cursor = _skip_whitespace(text, cursor)
if cursor >= close or text[cursor] != ":":
raise L10nError(f"expected ':' after key {key!r} at offset {cursor}")
cursor = _skip_whitespace(text, cursor + 1)
start = cursor
end = value_end(text, cursor)
entries.append(
JsEntry(key, key_start, start, end, line_indent(text, key_start))
)
cursor = end
continue
raise L10nError(
f"unexpected {char!r} at offset {cursor} inside an object literal"
)
return entries, close
def const_value_offset(text: str, name: str) -> int:
"""Return the offset of the value assigned to ``const <name>``."""
match = re.search(rf"\bconst\s+{re.escape(name)}\s*=\s*", text)
if not match:
raise L10nError(f"const {name} not found in web/index.html")
offset = match.end()
if offset >= len(text) or text[offset] not in PAIRS:
raise L10nError(f"const {name} is not initialized with a block")
return offset
def parse_document(text: str) -> Document:
"""Parse every localization structure out of web/index.html."""
keys_open = const_value_offset(text, "translationKeys")
keys_close = find_block(text, keys_open)
order = [
decode_js_string(token)
for token in STRING_RE.findall(text[keys_open + 1 : keys_close])
]
if len(set(order)) != len(order):
raise L10nError("translationKeys contains duplicate entries")
translations_open = const_value_offset(text, "translations")
translations_close = find_block(text, translations_open)
translations, _ = object_entries(text, translations_open)
object_locales: dict[str, list[tuple[str, str]]] = {}
object_indents: dict[str, tuple[str, str]] = {}
for entry in translations:
if text[entry.start] != "{":
raise L10nError(
f"translations[{entry.key}] must be an object literal, found "
f"{text[entry.start]!r}"
)
inner, _ = object_entries(text, entry.start)
pairs: list[tuple[str, str]] = []
for item in inner:
if text[item.start] not in "\"'`":
raise L10nError(
f"translations[{entry.key}].{item.key} must be a string literal"
)
pairs.append((item.key, decode_js_string(text[item.start : item.end])))
duplicates = [key for key, _ in pairs if [k for k, _ in pairs].count(key) > 1]
if duplicates:
raise L10nError(
f"translations[{entry.key}] has duplicate keys: {sorted(set(duplicates))}"
)
key_indent = inner[0].indent if inner else entry.indent + " "
object_locales[entry.key] = pairs
object_indents[entry.key] = (entry.indent, key_indent)
rows_open = const_value_offset(text, "translationRows")
rows_close = find_block(text, rows_open)
rows, _ = object_entries(text, rows_open)
row_locales: dict[str, list[str]] = {}
row_indents: dict[str, str] = {}
for entry in rows:
if text[entry.start] != "[":
raise L10nError(
f"translationRows[{entry.key}] must be an array literal, found "
f"{text[entry.start]!r}"
)
values = [
decode_js_string(token)
for token in STRING_RE.findall(
text[entry.start + 1 : find_block(text, entry.start)]
)
]
if len(values) != len(order):
raise L10nError(
f"translationRows[{entry.key}] has {len(values)} values but "
f"translationKeys has {len(order)}"
)
row_locales[entry.key] = values
row_indents[entry.key] = entry.indent
units_open = const_value_offset(text, "byteUnits")
units_close = find_block(text, units_open)
units, _ = object_entries(text, units_open)
byte_units: dict[str, list[str]] = {}
byte_unit_indents: dict[str, str] = {}
for entry in units:
if text[entry.start] != "[":
raise L10nError(
f"byteUnits[{entry.key}] must be an array literal, found "
f"{text[entry.start]!r}"
)
values = [
decode_js_string(token)
for token in STRING_RE.findall(
text[entry.start + 1 : find_block(text, entry.start)]
)
]
if len(values) != len(BYTE_UNIT_KEYS):
raise L10nError(
f"byteUnits[{entry.key}] has {len(values)} values but "
f"{len(BYTE_UNIT_KEYS)} were expected"
)
byte_units[entry.key] = values
byte_unit_indents[entry.key] = entry.indent
base_units = byte_units.get(BASE_LOCALE)
if base_units is not None and tuple(base_units) != BYTE_UNIT_BASE_VALUES:
raise L10nError(
f"byteUnits[{BASE_LOCALE}] is {base_units}, expected "
f"{list(BYTE_UNIT_BASE_VALUES)}"
)
labels = dict(OPTION_RE.findall(text))
return Document(
text=text,
order=order,
object_locales=object_locales,
object_indents=object_indents,
row_locales=row_locales,
row_indents=row_indents,
byte_units=byte_units,
byte_unit_indents=byte_unit_indents,
row_separator=string_separator(text, rows_open, rows_close),
unit_separator=string_separator(text, units_open, units_close),
labels=labels,
spans={
"translations": (translations_open, translations_close + 1),
"translationRows": (rows_open, rows_close + 1),
"byteUnits": (units_open, units_close + 1),
},
)
def english_comment(value: str) -> str:
"""Escape a value so it is safe inside a single-line // comment."""
return (
value.replace("\\", "\\\\")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
)
def render_catalog(
document: Document,
locale: str,
pairs: list[tuple[str, str]],
english: dict[str, str],
title: str,
command: str,
) -> str:
"""Render one locale as JSONC with an English comment above every value."""
lines = [
f"// ShadowMount+ web UI {title} - {locale}",
f"// {document.labels.get(locale, 'Unknown language')}",
"//",
"// Generated by tools/extract_web_l10n.py from web/index.html.",
f"// Edit the values, then run: {command}",
"//",
f'// Each entry is preceded by its "{BASE_LOCALE}" reference.',
"// Keep placeholders such as {title} or {shown} exactly as written.",
"",
"{",
]
for key, value in pairs:
reference = english.get(key)
if reference is not None:
lines.append(f" // English: {english_comment(reference)}")
lines.append(f" {js_string(key)}: {js_string(value)},")
lines[-1] = lines[-1][:-1]
lines.append("}")
return "\n".join(lines) + "\n"
def byte_unit_pairs(values: list[str] | None) -> list[tuple[str, str]]:
if values is None:
return [(key, "") for key in BYTE_UNIT_KEYS]
return list(zip(BYTE_UNIT_KEYS, values))
def catalog_pairs(document: Document, locale: str) -> list[tuple[str, str]]:
"""Return the key/value pairs of ``locale``.
Array locales deliberately omit the keys that exist only in the object-form
locales. ``translationKeys`` has no slot for them, so a value could never be
written back; emitting empty entries would only invite a translator to fill
in something that would then be rejected. They stay English-only.
"""
if locale in document.object_locales:
return list(document.object_locales[locale])
return list(zip(document.order, document.row_locales[locale]))
def english_reference(document: Document) -> dict[str, str]:
base = document.object_locales.get(BASE_LOCALE)
if base is None:
raise L10nError(f'translations["{BASE_LOCALE}"] is required as the reference')
return dict(base)
def byte_unit_reference(document: Document) -> dict[str, str]:
values = document.byte_units.get(BASE_LOCALE)
if values is None:
raise L10nError(f'byteUnits["{BASE_LOCALE}"] is required as the reference')
return dict(zip(BYTE_UNIT_KEYS, values))
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"locales",
nargs="*",
metavar="LOCALE",
help=f"locale to export, for example {BASE_LOCALE} or ar-SA",
)
parser.add_argument(
"--all",
action="store_true",
dest="all_locales",
help="export every locale found in web/index.html",
)
parser.add_argument(
"--byte-units",
action="store_true",
dest="byte_units",
help=f"also export the byte unit catalog as <LOCALE>{BYTE_UNITS_SUFFIX}; "
"with --all only locales that already define byte units are exported, "
"while naming a locale exports a blank template to fill in",
)
parser.add_argument(
"--html", type=Path, default=DEFAULT_HTML, help="source web/index.html"
)
parser.add_argument(
"--out-dir",
type=Path,
default=DEFAULT_OUT_DIR,
help="directory that receives the generated JSONC files",
)
parser.add_argument(
"--check",
action="store_true",
help="do not write anything; exit 1 if any file is out of date",
)
return parser
def main() -> int:
args = build_parser().parse_args()
if args.all_locales and args.locales:
print("error: pass locales or --all, not both", file=sys.stderr)
return 1
if not args.all_locales and not args.locales:
print("error: no locale given; pass a locale or --all", file=sys.stderr)
return 1
try:
text, newline = read_source(args.html)
document = parse_document(text)
except L10nError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if args.all_locales:
locales = document.locales
else:
locales = list(dict.fromkeys(args.locales))
errors: list[str] = []
for locale in locales:
if not LOCALE_RE.match(locale):
errors.append(f"{locale}: not a locale such as ar-SA")
continue
if locale not in document.object_locales and locale not in document.row_locales:
available = ", ".join(document.locales)
errors.append(f"{locale}: unknown locale; available locales: {available}")
continue
try:
english = english_reference(document)
except L10nError as exc:
errors.append(str(exc))
break
path = args.out_dir / f"{locale}.jsonc"
content = render_catalog(
document,
locale,
catalog_pairs(document, locale),
english,
"translations",
f"python tools/merge_web_l10n.py {locale}",
)
if not emit(path, content, newline, args.check):
errors.append(f"{locale}: {path.name} is out of date")
if args.byte_units and (locale in document.byte_units or not args.all_locales):
units = byte_unit_pairs(document.byte_units.get(locale))
unit_path = args.out_dir / f"{locale}{BYTE_UNITS_SUFFIX}"
try:
unit_english = byte_unit_reference(document)
except L10nError as exc:
errors.append(str(exc))
continue
unit_content = render_catalog(
document,
locale,
units,
unit_english,
"byte units",
f"python tools/merge_web_l10n.py {locale} --byte-units",
)
if not emit(unit_path, unit_content, newline, args.check):
errors.append(f"{locale}: {unit_path.name} is out of date")
if errors:
print("Extraction failed:", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
return 1
verb = "checked" if args.check else "wrote"
print(f"{verb} {len(locales)} locale(s) in {args.out_dir}")
return 0
def emit(path: Path, content: str, newline: str, check: bool) -> bool:
"""Write ``content`` unless --check was given; report whether it is current."""
if check:
try:
current, _ = read_source(path)
except L10nError:
return False
return current == content
write_text(path, content, newline)
return True
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, ValueError):
pass
raise SystemExit(main())
+411
View File
@@ -0,0 +1,411 @@
#!/usr/bin/env python3
"""Write edited JSONC translation catalogs back into web/index.html.
web/index.html keeps en-US and ru-RU as key/value objects inside
``translations`` and every other locale as a positional array inside
``translationRows`` indexed by ``translationKeys``. This tool reads the JSONC
files produced by tools/extract_web_l10n.py, validates them against the HTML,
and splices the values back into their original shape. Everything outside the
three localization blocks is left byte-for-byte identical.
Usage:
python tools/merge_web_l10n.py ar-SA
python tools/merge_web_l10n.py ru-RU --byte-units
python tools/merge_web_l10n.py ar-SA ru-RU --dry-run
python tools/merge_web_l10n.py --all --check
Nothing is written unless every requested locale validates.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from extract_web_l10n import (
BYTE_UNIT_KEYS,
BYTE_UNITS_SUFFIX,
DEFAULT_HTML,
DEFAULT_OUT_DIR,
LOCALE_RE,
Document,
L10nError,
closing_indent,
js_string,
parse_document,
read_source,
write_text,
)
def strip_line_comment(line: str) -> str:
"""Remove a // comment that sits outside any string literal."""
in_string = False
escaped = False
for index, char in enumerate(line):
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char == "/" and line[index : index + 2] == "//":
return line[:index]
return line
def load_jsonc(path: Path) -> dict[str, str]:
"""Parse a JSONC catalog file into a plain key/value mapping."""
try:
text, _ = read_source(path)
except L10nError as exc:
raise L10nError(str(exc)) from exc
stripped = "\n".join(strip_line_comment(line) for line in text.split("\n"))
try:
data = json.loads(stripped)
except json.JSONDecodeError as exc:
raise L10nError(f"{path.name} is not valid JSONC: {exc}") from exc
if not isinstance(data, dict):
raise L10nError(f"{path.name} must contain a single JSONC object")
for key, value in data.items():
if not isinstance(value, str):
raise L10nError(f"{path.name}: {key} must be a string")
return data
def render_object_locale(
locale: str, pairs: list[tuple[str, str]], indent: str, key_indent: str
) -> str:
lines = [f'{indent}"{locale}": {{']
for key, value in pairs:
lines.append(f"{key_indent}{key}: {js_string(value)},")
lines[-1] = lines[-1][:-1]
lines.append(indent + "}")
return "\n".join(lines)
def render_array_row(
locale: str, values: list[str], indent: str, separator: str
) -> str:
inner = separator.join(js_string(value) for value in values)
return f'{indent}"{locale}": [{inner}]'
def render_region(
text: str, open_index: int, close_index: int, blocks: list[str]
) -> str:
"""Rebuild one block, keeping the original closing delimiter indentation."""
return "{\n" + ",\n".join(blocks) + "\n" + closing_indent(text, close_index) + "}"
def resolve_locales(
document: Document, requested: list[str], all_locales: bool, errors: list[str]
) -> list[str]:
if all_locales:
return document.locales
valid: list[str] = []
for locale in requested:
if not LOCALE_RE.match(locale):
errors.append(f"{locale}: not a locale such as ar-SA")
elif (
locale not in document.object_locales and locale not in document.row_locales
):
errors.append(
f"{locale}: unknown locale; available locales: "
f"{', '.join(document.locales)}"
)
elif locale not in valid:
valid.append(locale)
return valid
def merge_catalog(
document: Document, locale: str, path: Path, errors: list[str]
) -> list[tuple[str, str]] | None:
"""Validate one JSONC catalog and return the pairs to write back."""
try:
edited = load_jsonc(path)
except L10nError as exc:
errors.append(f"{locale}: {exc}")
return None
if locale in document.object_locales:
current = document.object_locales[locale]
required = [key for key, _ in current]
unmergeable: list[str] = []
else:
current = list(zip(document.order, document.row_locales[locale]))
required = list(document.order)
unmergeable = document.orphan_keys
known = {key for key, _ in current}
allowed = known | set(unmergeable)
unknown = sorted(set(edited) - allowed)
if unknown:
errors.append(f"{locale}: unknown keys: {unknown}")
return None
missing = [key for key in required if key not in edited]
if missing:
errors.append(f"{locale}: missing keys: {missing}")
return None
blocked = sorted(key for key in unmergeable if edited.get(key, "").strip())
if blocked:
errors.append(
f"{locale}: {blocked} are absent from translationKeys, so "
"translationRows has no slot for them and the value would be lost"
)
return None
return [(key, edited[key]) for key, _ in current]
def merge_byte_units(
document: Document, locale: str, path: Path, errors: list[str]
) -> list[str] | None:
"""Validate one JSONC byte unit catalog and return the values to write back."""
try:
edited = load_jsonc(path)
except L10nError as exc:
errors.append(f"{locale}: {exc}")
return None
unknown = sorted(set(edited) - set(BYTE_UNIT_KEYS))
if unknown:
errors.append(f"{locale}: unknown byte unit keys: {unknown}")
return None
missing = [key for key in BYTE_UNIT_KEYS if key not in edited]
if missing:
errors.append(f"{locale}: missing byte unit keys: {missing}")
return None
values = [edited[key] for key in BYTE_UNIT_KEYS]
if locale not in document.byte_units:
if not any(values):
return None
if not all(values):
empty = sorted(
BYTE_UNIT_KEYS[index] for index, value in enumerate(values) if not value
)
errors.append(
f"{locale}: byteUnits has no entry for this locale, so either fill "
f"in all {len(BYTE_UNIT_KEYS)} values or clear the file; empty: {empty}"
)
return None
return values
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"locales",
nargs="*",
metavar="LOCALE",
help="locale to merge, for example ar-SA",
)
parser.add_argument(
"--all",
action="store_true",
dest="all_locales",
help="merge every locale that has a JSONC file",
)
parser.add_argument(
"--byte-units",
action="store_true",
dest="byte_units",
help=f"also merge <LOCALE>{BYTE_UNITS_SUFFIX}; with --all only locales "
"that already define byte units are merged, while naming a locale "
"requires the file and adds the locale to byteUnits if it is new",
)
parser.add_argument(
"--html", type=Path, default=DEFAULT_HTML, help="target web/index.html"
)
parser.add_argument(
"--l10n-dir",
type=Path,
default=DEFAULT_OUT_DIR,
help="directory holding the JSONC files",
)
parser.add_argument(
"--check",
action="store_true",
help="do not write anything; exit 1 if web/index.html is out of date",
)
parser.add_argument(
"--dry-run",
action="store_true",
dest="dry_run",
help="report what would change without writing",
)
return parser
def main() -> int:
args = build_parser().parse_args()
if args.all_locales and args.locales:
print("error: pass locales or --all, not both", file=sys.stderr)
return 1
if not args.all_locales and not args.locales:
print("error: no locale given; pass a locale or --all", file=sys.stderr)
return 1
errors: list[str] = []
try:
text, newline = read_source(args.html)
document = parse_document(text)
locales = resolve_locales(document, args.locales, args.all_locales, errors)
except L10nError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
object_pairs: dict[str, list[tuple[str, str]]] = {}
row_values: dict[str, list[str]] = {}
unit_values: dict[str, list[str]] = {}
changed = 0
for locale in locales:
catalog_path = args.l10n_dir / f"{locale}.jsonc"
if not catalog_path.is_file():
errors.append(
f"{locale}: {catalog_path} is missing; run "
f"python tools/extract_web_l10n.py {locale} first"
)
continue
pairs = merge_catalog(document, locale, catalog_path, errors)
if pairs is None:
continue
if locale in document.object_locales:
if pairs != document.object_locales[locale]:
changed += 1
object_pairs[locale] = pairs
else:
values = [value for _, value in pairs]
if values != document.row_locales[locale]:
changed += 1
row_values[locale] = values
if args.byte_units and (locale in document.byte_units or not args.all_locales):
unit_path = args.l10n_dir / f"{locale}{BYTE_UNITS_SUFFIX}"
if not unit_path.is_file():
errors.append(
f"{locale}: {unit_path} is missing; run "
f"python tools/extract_web_l10n.py {locale} --byte-units first"
)
continue
values = merge_byte_units(document, locale, unit_path, errors)
if values is None:
continue
if document.byte_units.get(locale) != values:
changed += 1
unit_values[locale] = values
if errors:
print("Merge failed; web/index.html was not modified:", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
return 1
edits: list[tuple[int, int, str]] = []
def replace_region(name: str, blocks: list[str]) -> None:
open_index, end_index = document.spans[name]
close_index = end_index - 1
edits.append(
(
open_index,
end_index,
render_region(text, open_index, close_index, blocks),
)
)
if object_pairs:
merged = dict(document.object_locales)
merged.update(object_pairs)
replace_region(
"translations",
[
render_object_locale(
locale, merged[locale], *document.object_indents[locale]
)
for locale in merged
],
)
if row_values:
merged = dict(document.row_locales)
merged.update(row_values)
replace_region(
"translationRows",
[
render_array_row(
locale,
merged[locale],
document.row_indents[locale],
document.row_separator,
)
for locale in merged
],
)
if unit_values:
merged = dict(document.byte_units)
merged.update(unit_values)
indent = next(iter(document.byte_unit_indents.values()))
replace_region(
"byteUnits",
[
render_array_row(
locale,
merged[locale],
document.byte_unit_indents.get(locale, indent),
document.unit_separator,
)
for locale in merged
],
)
if not edits:
print("Nothing to merge.", file=sys.stderr)
return 1
result = text
for start, end, replacement in sorted(edits, reverse=True):
result = result[:start] + replacement + result[end:]
if args.dry_run:
print(f"Would update {changed} of {len(locales)} locale(s) in {args.html}")
return 0
if args.check:
if result == text:
print(f"{args.html} is up to date with {args.l10n_dir}")
return 0
print(
f"error: {args.html} is out of date with {args.l10n_dir}", file=sys.stderr
)
return 1
write_text(args.html, result, newline)
print(
f"Merged {len(locales)} locale(s), {changed} changed, into {args.html}\n"
"Note: web/index.html is embedded into src/web_index_asset.c; rebuild "
"before deploying to the console."
)
return 0
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, ValueError):
pass
raise SystemExit(main())
+3 -2
View File
@@ -922,7 +922,7 @@
"pl-PL": ["Biblioteka ShadowMount","Łączę z API…","Sprawdzanie","Sterowanie biblioteką","Nazwa, identyfikator tytułu, identyfikator treści lub ścieżka","Filtr biblioteczny","Wszystkie gry","Foldery","Obrazy","Język","Oblicz rozmiar","Odśwież","Skanuj","Podsumowanie biblioteki","Razem gry","Zamontowany","Biblioteka","Ładowanie biblioteki…","Przechowywanie","Wolne miejsce w dostępnych punktach montażu","Ładowanie…","Operacje na danych","Pobieram stan operacji…","Postęp operacji","Rozmiar","Pliki","Prędkość","/s","Czas","Anuluj","Bezpośrednie ścieżki do poszczególnych folderów gier lub obrazów","Dodaj","Ustawienia","Dziennik debugowania","Tryb cichy","Zaktualizuj emulatory gry Fakelib","Zaktualizuj emulator AMPR z GitHub","Usuń niedostępne ikony aplikacji","Opóźnienie usunięcia (sekundy)","Zezwól na dostęp do sieci lokalnej","Cel fanów","Sterowanie systemem","Zapisz ustawienia","Ustawienia zapisane; przeładowanie środowiska uruchomieniowego w kolejce","Wyświetl dziennik debugowania","Wyświetl dziennik jądra","Dziennik debugowania","Dziennik jądra","Wyświetlanie {shown} z {total}","Skanuj ścieżki","Niestandardowe korzenie rekurencyjne; pusta lista używa wbudowanych wartości domyślnych","Ścieżka skanowania dodana lokalnie; zapisz ustawienia, aby je zastosować","Ścieżka skanowania została usunięta lokalnie; zapisz ustawienia, aby je zastosować","Wprowadź ścieżkę bezwzględną inną niż główna","Opcje skanowania","Zresetuj liczniki prób","Ta opcja dotyczy tylko następnego wymuszonego skanowania.","Ostatnie operacje","Nie ma jeszcze żadnych operacji","Informacje o grze","Zamknij","Osłona na {title}","nieznany","folderze","obraz","Źródło niedostępne","Szczegóły","Góra","Odmontuj","Nic nie znaleziono","Biblioteka jest pusta","Pokazano: {count}","Brak pracy","Przygotowanie","Pomiar","Usuwanie","Finalizowanie","Ukończono","Nie udało się","Anulowano","Kopiowanie","Poruszanie się","Usuwanie","Rozpakowywanie","Brak aktywnych operacji","Obliczanie…","Anuluję…","{operation} {title} ukończono","{operation} ukończono","{operation} {title} anulowane","{operation} anulowane","błąd {status}","{operation} {title}: {error}","Niedostępne","Zażądano anulowania zadania {id}","Nie znaleziono miejsca na dane","Bezpłatny {size}","z {size}","Używana przestrzeń w {path}","Podłączono PS5","Brak połączenia","Błąd API: {error}","Lista jest pusta","Usuń","Źródło dodane do manual.lst","Źródło znajduje się już w manual.lst","Usunąć to źródło z manual.lst?\n\n{path}","Źródło usunięte z manual.lst","Skanowanie w kolejce ze zresetowanymi licznikami prób","Skanowanie w kolejce","Skanowanie rozpoczęte","Zamontowany {title}{mode}","Odmontowany {title}","Źródło","Ścieżka środowiska wykonawczego","Błąd {status}","Zainstalowany","Ostatnie uruchomienie","Zamontuj R/O","Zamontuj R/W","Kopiuj","Rusz się","Rozpakuj","Usuń źródło","Poczekaj na zakończenie bieżącej operacji przechowywania","Odinstaluj","Obliczanie rozmiaru źródła…","Operacja przechowywania jest już uruchomiona","Folder docelowy do kopiowania:","Folder docelowy do przeniesienia:","Folder docelowy do rozpakowania. Zostanie utworzony podfolder {title}:","Usunąć obraz źródłowy dopiero po pomyślnym zakończeniu rozpakowywania?\n\nAnuluj zachowuje obraz.","{operation} {title} rozpoczęło się jako zadanie {id}","Rozpoczęto operację","Poprosić o odinstalowanie {game}?\n\nObraz źródłowy lub folder nie zostaną usunięte.","Żądanie odinstalowania dla {title} zostało zaakceptowane","Trwale usunąć fizyczne źródło {game}?\n\nTego nie można cofnąć."],
"pt-BR": ["Biblioteca ShadowMount","Conectando à API…","Verificando","Controles de biblioteca","Nome, ID do título, ID do conteúdo ou caminho","Filtro de biblioteca","Todos os jogos","Pastas","Imagens","Idioma","Calcular tamanho","Atualizar","Digitalizar","Resumo da biblioteca","Total de jogos","Montado","Biblioteca","Carregando biblioteca…","Armazenamento","Espaço livre nos pontos de montagem disponíveis","Carregando…","Operações de dados","Obtendo status da operação…","Progresso da operação","Tamanho","Arquivos","Velocidade","/s","Hora","Cancelar","Caminhos diretos para pastas ou imagens individuais do jogo","Adicionar","Configurações","Registro de depuração","Modo silencioso","Atualizar emuladores fakelib do jogo","Atualize o emulador AMPR do GitHub","Remova ícones de aplicativos indisponíveis","Atraso de remoção (segundos)","Permitir acesso à rede local","Alvo dos fãs","Controlado pelo sistema","Salvar configurações","Configurações salvas; recarga de tempo de execução na fila","Ver log de depuração","Ver log do kernel","Registro de depuração","Registro do kernel","Mostrando {shown} de {total}","Caminhos de verificação","Raízes recursivas personalizadas; uma lista vazia usa padrões internos","Caminho de digitalização adicionado localmente; salve as configurações para aplicar","Caminho de digitalização removido localmente; salve as configurações para aplicar","Insira um caminho absoluto não raiz","Opções de digitalização","Redefinir contadores de tentativas","Esta opção aplica-se apenas à próxima verificação forçada.","Operações recentes","Nenhuma operação ainda","Informações do jogo","Fechar","Capa para {title}","desconhecido","pasta","imagem","Fonte indisponível","Detalhes","Montar","Desmontar","Nada encontrado","A biblioteca está vazia","Mostrado: {count}","Sem trabalho","Preparando","Medindo","Excluindo","Finalizando","Concluído","Falha","Cancelado","Copiando","Movendo-se","Excluindo","Desempacotando","Nenhuma operação ativa","Calculando…","Cancelando…","{operation} {title} concluído","{operation} concluído","{operation} {title} cancelado","{operation} cancelado","erro {status}","{operation} {title}: {error}","Indisponível","Cancelamento solicitado para trabalho {id}","Nenhum armazenamento encontrado","Grátis {size}","de {size}","Espaço usado em {path}","PS5 conectado","Sem conexão","Erro de API: {error}","A lista está vazia","Remover","Fonte adicionada a manual.lst","A fonte já está em manual.lst","Remover esta fonte de manual.lst?\n\n{path}","Fonte removida de manual.lst","Varredura na fila com contadores de tentativas redefinidos","Verificação na fila","Verificação iniciada","Montado {title}{mode}","Desmontado {title}","Fonte","Caminho do tempo de execução","Erro {status}","Instalado","Último lançamento","Montar R/O","Montagem R/W","Copiar","Mover","Descompacte","Excluir fonte","Aguarde a conclusão da operação de armazenamento atual","Desinstalar","Calculando o tamanho da fonte…","Uma operação de armazenamento já está em execução","Pasta de destino para cópia:","Pasta de destino para movimentação:","Pasta de destino para descompactação. Uma subpasta {title} será criada:","Excluir a imagem de origem somente após a descompactação ser concluída com êxito?\n\nCancelar mantém a imagem.","{operation} {title} iniciado como trabalho {id}","Operação iniciada","Solicitar desinstalação de {game}?\n\nA imagem ou pasta de origem não será excluída.","Solicitação de desinstalação para {title} aceita","Excluir permanentemente a fonte física de {game}?\n\nIsto não pode ser desfeito."],
"tr-TR": ["ShadowMount Kütüphanesi","API'ye bağlanılıyor…","Kontrol ediliyor","Kütüphane kontrolleri","Ad, Başlık Kimliği, İçerik Kimliği veya yol","Kitaplık filtresi","Tüm oyunlar","Klasörler","Görseller","Dil","Boyutu hesapla","Yenile","Tara","Kitaplık özeti","Toplam oyunlar","Monte edilmiş","Kütüphane","Kitaplık yükleniyor…","Depolama","Mevcut bağlama noktalarında boş alan","Yükleniyor…","Veri işlemleri","Operasyon durumu alınıyor…","Operasyon ilerlemesi","Boyut","Dosyalar","Hız","/s","Zaman","İptal","Bireysel oyun klasörlerine veya görsellerine doğrudan yollar","Ekle","Ayarlar","Hata ayıklama günlüğü","Sessiz mod","Oyun fakelib emülatörlerini güncelleyin","AMPR öykünücüsünü GitHub'dan güncelleyin","Kullanılamayan uygulama simgelerini kaldırın","Kaldırma gecikmesi (saniye)","Yerel ağ erişimine izin ver","Hayran hedefi","Sistem kontrollü","Ayarları kaydet","Ayarlar kaydedildi; çalışma zamanı yeniden yüklemesi sıraya alındı","Hata ayıklama günlüğünü görüntüle","Çekirdek günlüğünü görüntüle","Hata ayıklama günlüğü","Çekirdek günlüğü","Gösterilen {shown} / {total}","Yolları tara","Özel özyinelemeli kökler; boş bir liste yerleşik varsayılanları kullanır","Tarama yolu yerel olarak eklendi; uygulamak için ayarları kaydedin","Tarama yolu yerel olarak kaldırıldı; uygulamak için ayarları kaydedin","Kök olmayan bir mutlak yol girin","Tarama seçenekleri","Deneme sayaçlarını sıfırla","Bu seçenek yalnızca bir sonraki zorunlu tarama için geçerlidir.","Son operasyonlar","Henüz işlem yok","Oyun bilgileri","Kapat","{title} için kapak","bilinmiyor","klasör","görüntü","Kaynak mevcut değil","Ayrıntılar","Montaj","Bağlantıyı kaldır","Hiçbir şey bulunamadı","Kütüphane boş","Gösterilen: {count}","iş yok","Hazırlanıyor","Ölçme","Silme","Sonlandırılıyor","Tamamlandı","Başarısız","İptal edildi","Kopyalanıyor","Hareketli","Silme","Paketi açma","Aktif işlem yok","Hesaplanıyor…","İptal ediliyor…","{operation} {title} tamamlandı","{operation} tamamlandı","{operation} {title} iptal edildi","{operation} iptal edildi","hata {status}","{operation} {title}: {error}","Kullanılamıyor","{id} işi için iptal istendi","Depolama alanı bulunamadı","Ücretsiz {size}","/{size}","{path} adresinde kullanılan alan","PS5 bağlandı","Bağlantı yok","API hatası: {error}","Liste boş","Kaldır","Kaynak manual.lst'e eklendi","Kaynak zaten manual.lst'te","Bu kaynak manual.lst'ten kaldırılsın mı?\n\n{path}","Kaynak manual.lst'ten kaldırıldı","Deneme sayaçları sıfırlanarak tarama sıraya alındı","Tarama sıraya alındı","Tarama başlatıldı","Monte edilmiş {title}{mode}","Bağlantısı kesildi {title}","Kaynak","Çalışma zamanı yolu","Hata {status}","Yüklendi","Son lansman","R/O'yu monte edin","R/W montajı","Kopyala","Taşı","Paketi aç","Kaynağı sil","Mevcut depolama işleminin bitmesini bekleyin","Kaldır","Kaynak boyutu hesaplanıyor…","Bir depolama işlemi zaten çalışıyor","Kopyalama için hedef klasör:","Taşıma için hedef klasör:","Paketi açmak için hedef klasör. Bir {title} alt klasörü oluşturulacak:","Kaynak görüntü yalnızca paketi açma başarıyla tamamlandıktan sonra silinsin mi?\n\nİptal görüntüyü korur.","{operation} {title}, {id} işi olarak başladı","Operasyon başladı","{game} uygulamasının kaldırılması istensin mi?\n\nKaynak görüntü veya klasör silinmeyecektir.","{title} için kaldırma isteği kabul edildi","{game} için fiziksel kaynak kalıcı olarak silinsin mi?\n\nBu geri alınamaz."],
"ar-SA": ["مكتبة ShadowMount","جارٍ الاتصال بواجهة برمجة التطبيقات…","التحقق","ضوابط المكتبة","الاسم أو معرف العنوان أو معرف المحتوى أو المسار","مرشح المكتبة","جميع الألعاب","المجلدات","الصور","اللغة","احسب الحجم","تحديث","مسح","ملخص المكتبة","مجموع الألعاب","شنت","مكتبة","جارٍ تحميل المكتبة…","التخزين","مساحة حرة في نقاط التثبيت المتاحة","جارٍ التحميل…","عمليات البيانات","جارٍ الحصول على حالة التشغيل...","تقدم العملية","الحجم","ملفات","السرعة","/ث","الوقت","إلغاء","مسارات مباشرة إلى مجلدات أو صور اللعبة الفردية","أضف","الإعدادات","سجل التصحيح","الوضع الهادئ","تحديث محاكيات لعبة Fakelib","قم بتحديث محاكي AMPR من GitHub","إزالة أيقونات التطبيقات غير المتاحة","تأخير الإزالة (ثواني)","السماح بالوصول إلى الشبكة المحلية","هدف المروحة","يتم التحكم في النظام","حفظ الإعدادات","تم حفظ الإعدادات؛ إعادة تحميل وقت التشغيل في قائمة الانتظار","عرض سجل التصحيح","عرض سجل النواة","سجل التصحيح","سجل النواة","عرض {shown} من {total}","مسارات المسح","جذور العودية المخصصة؛ تستخدم القائمة الفارغة الإعدادات الافتراضية المضمنة","تمت إضافة مسار المسح محليًا؛ حفظ الإعدادات للتطبيق","تمت إزالة مسار المسح محليًا؛ حفظ الإعدادات للتطبيق","أدخل مسارًا مطلقًا غير جذري","خيارات المسح","إعادة ضبط عدادات المحاولات","ينطبق هذا الخيار فقط على الفحص القسري التالي.","العمليات الأخيرة","لا توجد عمليات حتى الآن","معلومات اللعبة","إغلاق","غطاء ل{title}","غير معروف","المجلد","صورة","المصدر غير متاح","التفاصيل","جبل","إلغاء التحميل","لم يتم العثور على شيء","المكتبة فارغة","يظهر: {count}","لا وظيفة","تحضير","القياس","حذف","وضع اللمسات النهائية","مكتمل","فشل","تم الإلغاء","النسخ","تتحرك","حذف","التفريغ","لا توجد عمليات نشطة","جارٍ الحساب…","جارٍ الإلغاء…","{operation} {title} اكتمل","{operation} اكتمل","{operation} {title} تم الإلغاء","{operation} تم الإلغاء","خطأ {status}","{operation} {title}: {error}","غير متاح","طلب الإلغاء للوظيفة {id}","لم يتم العثور على مساحة تخزين","مجاني {size}","من {size}","المساحة المستخدمة في {path}","تم توصيل PS5","لا يوجد اتصال","خطأ في واجهة برمجة التطبيقات: {error}","القائمة فارغة","إزالة","تمت إضافة المصدر إلى manual.lst","المصدر موجود بالفعل في manual.lst","هل تريد إزالة هذا المصدر من manual.lst؟\n\n{path}","تمت إزالة المصدر من manual.lst","تم وضع المسح الضوئي في قائمة الانتظار مع إعادة تعيين عدادات المحاولات","المسح في قائمة الانتظار","بدأ المسح","مثبتة {title}{mode}","غير مثبتة {title}","المصدر","مسار وقت التشغيل","خطأ {status}","تم التثبيت","الإطلاق الأخير","جبل R/O","جبل آر / دبليو","نسخ","تحرك","فك","حذف المصدر","انتظر حتى تنتهي عملية التخزين الحالية","إلغاء التثبيت","جارٍ حساب حجم المصدر…","عملية تخزين قيد التشغيل بالفعل","المجلد الوجهة للنسخ:","المجلد الوجهة للتحرك:","المجلد الوجهة للتفريغ. سيتم إنشاء مجلد فرعي {title}:","هل تريد حذف الصورة المصدر فقط بعد اكتمال عملية التفريغ بنجاح؟\n\nإلغاء يحافظ على الصورة.","{operation} {title} بدأ كوظيفة {id}","بدأت العملية","هل تريد إلغاء تثبيت {game}؟\n\nلن يتم حذف الصورة أو المجلد المصدر.","تم قبول طلب إلغاء تثبيت {title}","هل تريد حذف المصدر الفعلي لـ {game} نهائيًا؟\n\nلا يمكن التراجع عن هذا."],
"ar-SA": ["مكتبة ShadowMount","جارٍ الاتصال بـ API…","جارٍ التحقق","أدوات المكتبة","الاسم أو معرّف العنوان أو معرّف المحتوى أو المسار","تصفية المكتبة","جميع الألعاب","المجلدات","صور الأقراص","اللغة","احسب الحجم","تحديث","مسح","ملخص المكتبة","إجمالي الألعاب","مُركَّب","المكتبة","جارٍ تحميل المكتبة…","التخزين","المساحة الفارغة في نقاط التركيب المتاحة","جارٍ التحميل…","عمليات البيانات","جارٍ جلب حالة العملية…","تقدّم العملية","الحجم","الملفات","السرعة","/ث","الوقت","إلغاء","مسارات مباشرة إلى مجلدات أو صور أقراص الألعاب الفردية","إضافة","الإعدادات","سجل التصحيح","الوضع الصامت","تحديث محاكيات fakelib للألعاب","تحديث محاكي AMPR من GitHub","إزالة أيقونات التطبيقات غير المتاحة","تأخير الإزالة (ثوانٍ)","السماح بالوصول إلى الشبكة المحلية","درجة حرارة المروحة المستهدفة","تحكّم النظام","حفظ الإعدادات","تم حفظ الإعدادات؛ أُدرجت إعادة تحميل التشغيل في قائمة الانتظار","عرض سجل التصحيح","عرض سجل النواة","سجل التصحيح","سجل النواة","معروض {shown} من {total}","مسارات المسح","جذور متكررة مخصصة؛ القائمة الفارغة تستخدم المسارات الافتراضية المضمّنة","أُضيف مسار المسح محليًا؛ احفظ الإعدادات للتطبيق","أُزيل مسار المسح محليًا؛ احفظ الإعدادات للتطبيق","أدخل مسارًا مطلقًا غير جذري","خيارات المسح","إعادة ضبط عدادات المحاولات","ينطبق هذا الخيار على المسح الإجباري التالي فقط.","العمليات الأخيرة","لا توجد عمليات بعد","معلومات اللعبة","إغلاق","غلاف {title}","غير معروف","مجلد","صورة قرص","المصدر غير متاح","التفاصيل","تركيب","إلغاء التركيب","لا توجد نتائج","المكتبة فارغة","معروض: {count}","لا مهام","جارٍ التحضير","جارٍ القياس","جارٍ الحذف","جارٍ الإنهاء","اكتملت","فشلت","أُلغيت","جارٍ النسخ","جارٍ النقل","جارٍ الحذف","جارٍ الاستخراج","لا توجد عمليات نشطة","جارٍ الحساب…","جارٍ الإلغاء…","اكتملت {operation} {title}","اكتملت {operation}","أُلغيت {operation} {title}","أُلغيت {operation}","خطأ {status}","{operation} {title}: {error}","غير متاح","طُلب إلغاء المهمة {id}","لم يتم العثور على وحدة تخزين","متاح {size}","من أصل {size}","المساحة المستخدمة في {path}","PS5 متصل","PS5 غير متصل","خطأ API: {error}","القائمة فارغة","إزالة","أُضيف المصدر إلى manual.lst","المصدر موجود بالفعل في manual.lst","هل تريد إزالة هذا المصدر من manual.lst؟\n\n{path}","أُزيل المصدر من manual.lst","أُدرج المسح في قائمة الانتظار مع إعادة ضبط عدادات المحاولات","أُدرج المسح في قائمة الانتظار","بدأ المسح","تم تركيب {title}{mode}","تم إلغاء تركيب {title}","المصدر","مسار التشغيل","خطأ {status}","تاريخ التثبيت","آخر تشغيل","تركيب R/O","تركيب R/W","نسخ","نقل","استخراج","حذف المصدر","انتظر حتى تنتهي عملية التخزين الحالية","إلغاء التثبيت","جارٍ حساب حجم المصدر…","هناك عملية تخزين جارية","مجلد الوجهة للنسخ:","مجلد الوجهة للنقل:","مجلد الوجهة للاستخراج. سيتم إنشاء مجلد فرعي باسم {title}:","هل تريد حذف صورة المصدر فقط بعد اكتمال الاستخراج بنجاح؟\n\n«إلغاء» يحتفظ بصورة القرص.","بدأت {operation} {title} كمهمة {id}","بدأت العملية","هل تريد طلب إلغاء تثبيت {game}؟\n\nلن يتم حذف صورة القرص أو المجلد المصدر.","تم قبول طلب إلغاء تثبيت {title}","هل تريد حذف المصدر الفعلي لـ {game} نهائيًا؟\n\nلا يمكن التراجع عن هذا."],
"cs-CZ": ["Knihovna ShadowMount","Připojování k API…","Kontrola","Ovládací prvky knihovny","Název, ID titulu, ID obsahu nebo cesta","Filtr knihovny","Všechny hry","Složky","Obrázky","Jazyk","Vypočítejte velikost","Obnovit","Skenujte","Shrnutí knihovny","Celkem her","Namontováno","Knihovna","Načítání knihovny…","Skladování","Volné místo v dostupných přípojných bodech","Načítání…","Datové operace","Načítání stavu operace…","Průběh operace","Velikost","Soubory","Rychlost","/s","Čas","Zrušit","Přímé cesty k jednotlivým složkám hry nebo obrázkům","Přidat","Nastavení","Protokol ladění","Tichý režim","Aktualizujte emulátory fakelib hry","Aktualizujte emulátor AMPR z GitHubu","Odstraňte ikony nedostupných aplikací","Zpoždění odstranění (sekundy)","Povolit přístup k místní síti","Cíl fanoušků","Řízený systémem","Uložte nastavení","Nastavení uloženo; opětovné načtení za běhu ve frontě","Zobrazit protokol ladění","Zobrazit protokol jádra","Protokol ladění","Protokol jádra","Zobrazeno {shown} z {total}","Skenování cest","Vlastní rekurzivní kořeny; prázdný seznam používá vestavěné výchozí hodnoty","Cesta skenování přidána místně; uložit nastavení pro použití","Cesta skenování byla místně odstraněna; uložit nastavení pro použití","Zadejte nekořenovou absolutní cestu","Možnosti skenování","Vynulujte počítadla pokusů","Tato možnost platí pouze pro příští vynucené skenování.","Nedávné operace","Zatím žádné operace","Informace o hře","Zavřít","Obal pro {title}","neznámý","složku","obrázek","Zdroj nedostupný","Podrobnosti","Mount","Odpojit","Nic nenalezeno","Knihovna je prázdná","Zobrazeno: {count}","Žádná práce","Příprava","Měření","Mazání","Finalizace","Dokončeno","Nepodařilo se","Zrušeno","Kopírování","Stěhování","Mazání","Vybalování","Žádné aktivní operace","Počítání…","Rušení…","{operation} {title} dokončeno","{operation} dokončeno","{operation} {title} zrušeno","{operation} zrušeno","chyba {status}","{operation} {title}: {error}","Není k dispozici","Žádost o zrušení úlohy {id}","Nebylo nalezeno žádné úložiště","Zdarma {size}","z {size}","Využité místo na {path}","PS5 připojeno","Žádné spojení","Chyba API: {error}","Seznam je prázdný","Odebrat","Zdroj přidán do manual.lst","Zdroj je již v manual.lst","Odebrat tento zdroj z manual.lst?\n\n{path}","Zdroj odstraněn z manual.lst","Skenování ve frontě s vynulováním počítadel pokusů","Skenování ve frontě","Skenování začalo","Namontováno {title}{mode}","Odpojeno {title}","Zdroj","Cesta běhu","Chyba {status}","Instalováno","Poslední spuštění","Montáž R/O","Montáž R/W","Kopírovat","Pohybujte se","Rozbalit","Smazat zdroj","Počkejte na dokončení aktuální operace úložiště","Odinstalovat","Výpočet velikosti zdroje…","Operace úložiště již běží","Cílová složka pro kopírování:","Cílová složka pro přesun:","Cílová složka pro rozbalení. Bude vytvořena podsložka {title}:","Smazat zdrojový obrázek až po úspěšném rozbalení?\n\nZrušit zachová obrázek.","{operation} {title} zahájeno jako úloha {id}","Provoz zahájen","Požádat o odinstalaci {game}?\n\nZdrojový obrázek nebo složka nebudou odstraněny.","Požadavek na odinstalaci pro {title} byl přijat","Trvale odstranit fyzický zdroj pro {game}?\n\nToto nelze vrátit zpět."],
"hu-HU": ["ShadowMount könyvtár","Csatlakozás az API-hoz…","Ellenőrzés","Könyvtári vezérlők","Név, címazonosító, tartalomazonosító vagy elérési út","Könyvtári szűrő","Minden játék","Mappák","Képek","Nyelv","Számítsa ki a méretet","Frissítés","Scan","Könyvtári összefoglaló","Összes játék","Felszerelve","Könyvtár","Könyvtár betöltése…","Tárolás","Szabad hely a rendelkezésre álló rögzítési pontokon","Betöltés…","Adatműveletek","Működési állapot lekérése…","A művelet előrehaladása","Méret","Fájlok","Sebesség","/s","Idő","Mégse","Közvetlen utak az egyes játékmappákhoz vagy képekhez","Hozzáadás","Beállítások elemre","Hibakeresési napló","Csendes üzemmód","Frissítse a játék fakelib emulátorait","Frissítse az AMPR emulátort a GitHubról","Távolítsa el a nem elérhető alkalmazásikonokat","Eltávolítási késleltetés (másodperc)","Helyi hálózati hozzáférés engedélyezése","Rajongói célpont","Rendszer vezérelt","Mentse el a beállításokat","A beállítások elmentve; futásidejű újratöltési sor","Hibakeresési napló megtekintése","Kernelnapló megtekintése","Hibakeresési napló","Kernelnapló","{shown} / {total} megjelenítése","Útvonalak vizsgálata","Egyéni rekurzív gyökerek; egy üres lista beépített alapértelmezett értékeket használ","Keresési útvonal helyileg hozzáadva; mentse a beállításokat az alkalmazáshoz","A vizsgálati útvonal helyben eltávolítva; mentse a beállításokat az alkalmazáshoz","Adjon meg egy nem gyökér abszolút elérési utat","Szkennelési beállítások","Reset kísérletszámlálók","Ez az opció csak a következő kényszerített vizsgálatra vonatkozik.","Legutóbbi műveletek","Még nincs művelet","Játékinformációk","Bezárás","Borító: {title}","ismeretlen","mappát","kép","A forrás nem elérhető","Részletek","Mount","Leválasztás","Semmi sem található","A könyvtár üres","Megjelenítve: {count}","Nincs munka","Felkészülés","Mérés","Törlés","Véglegesítés","Befejezve","Sikertelen","Törölve","Másolás","Mozgás","Törlés","Kicsomagolás","Nincsenek aktív műveletek","Számítás…","Lemondás…","{operation} {title} befejeződött","{operation} befejeződött","{operation} {title} törölve","{operation} törölve","hiba {status}","{operation} {title}: {error}","Nem elérhető","Lemondást kértek a munkához {id}","Nem található tárhely","Ingyenes {size}","{size}","Használt hely a {path}","PS5 csatlakoztatva","Nincs kapcsolat","API hiba: {error}","A lista üres","Távolítsa el","A forrás hozzáadva a manual.lst-hez","A forrás már megtalálható a manual.lst fájlban","Eltávolítja ezt a forrást a manual.lst fájlból?\n\n{path}","A forrás eltávolítva a manual.lst fájlból","Keresés sorba állva, kísérletszámlálók nullázva","Beolvasás sorban","A szkennelés elindult","Szerelt {title}{mode}","Lecsatolt {title}","Forrás","Futási útvonal","Hiba {status}","Telepítve","Utolsó indítás","Szerelje fel az R/O-t","R/W rögzítés","Másolás","Mozgás","Csomagolja ki","Forrás törlése","Várja meg, amíg az aktuális tárolási művelet befejeződik","Eltávolítás","Forrás méretének kiszámítása…","Egy tárolási művelet már fut","Másolás célmappa:","Az áthelyezés célmappa:","A kicsomagolás célmappa. Létrejön egy {title} almappa:","Csak a kicsomagolás sikeres befejezése után törli a forrásképet?\n\nA Mégse funkció megtartja a képet.","{operation} {title} munkakörben indult {id}","A művelet megkezdődött","Kéri a {game} eltávolítását?\n\nA forráskép vagy mappa nem törlődik.","A {title} eltávolítási kérelme elfogadva","Véglegesen törli a {game} fizikai forrását?\n\nEzt nem lehet visszavonni."],
"el-GR": ["ShadowMount Library","Σύνδεση σε API…","Έλεγχος","Έλεγχοι βιβλιοθήκης","Όνομα, αναγνωριστικό τίτλου, αναγνωριστικό περιεχομένου ή διαδρομή","Φίλτρο βιβλιοθήκης","Όλα τα παιχνίδια","Φάκελοι","εικόνες","Γλώσσα","Υπολογίστε το μέγεθος","Ανανέωση","Σάρωση","Περίληψη βιβλιοθήκης","Σύνολο παιχνιδιών","Τοποθετημένο","Βιβλιοθήκη","Φόρτωση βιβλιοθήκης…","Αποθήκευση","Ελεύθερος χώρος στα διαθέσιμα σημεία τοποθέτησης","Φόρτωση…","Λειτουργίες δεδομένων","Λήψη κατάστασης λειτουργίας…","Πρόοδος λειτουργίας","Μέγεθος","Αρχεία","Ταχύτητα","/s","Χρόνος","Ακύρωση","Απευθείας διαδρομές σε μεμονωμένους φακέλους παιχνιδιών ή εικόνες","Προσθήκη","Ρυθμίσεις","Ημερολόγιο εντοπισμού σφαλμάτων","Αθόρυβη λειτουργία","Ενημερώστε τους εξομοιωτές του παιχνιδιού fakelib","Ενημερώστε τον εξομοιωτή AMPR από το GitHub","Καταργήστε τα μη διαθέσιμα εικονίδια εφαρμογών","Καθυστέρηση αφαίρεσης (δευτερόλεπτα)","Επιτρέψτε την πρόσβαση στο τοπικό δίκτυο","Στόχος θαυμαστής","Ελεγχόμενο σύστημα","Αποθήκευση ρυθμίσεων","Οι ρυθμίσεις αποθηκεύτηκαν. ουρά επαναφόρτωσης χρόνου εκτέλεσης","Προβολή αρχείου καταγραφής εντοπισμού σφαλμάτων","Προβολή αρχείου καταγραφής πυρήνα","Ημερολόγιο εντοπισμού σφαλμάτων","Ημερολόγιο πυρήνα","Εμφάνιση {shown} από {total}","Σάρωση μονοπατιών","Προσαρμοσμένες αναδρομικές ρίζες. μια κενή λίστα χρησιμοποιεί ενσωματωμένες προεπιλογές","Η διαδρομή σάρωσης προστέθηκε τοπικά. αποθηκεύστε τις ρυθμίσεις για εφαρμογή","Η διαδρομή σάρωσης καταργήθηκε τοπικά. αποθηκεύστε τις ρυθμίσεις για εφαρμογή","Εισαγάγετε μια απόλυτη διαδρομή χωρίς ρίζα","Επιλογές σάρωσης","Επαναφορά μετρητών προσπάθειας","Αυτή η επιλογή ισχύει μόνο για την επόμενη αναγκαστική σάρωση.","Πρόσφατες λειτουργίες","Δεν υπάρχουν ακόμη επεμβάσεις","Πληροφορίες παιχνιδιού","Κλείσιμο","Κάλυμμα για {title}","άγνωστο","φάκελο","εικόνα","Η πηγή δεν είναι διαθέσιμη","Λεπτομέρειες","Όρος","Αποπροσάρτηση","Δεν βρέθηκε τίποτα","Η βιβλιοθήκη είναι άδεια","Εμφανίζεται: {count}","Καμία δουλειά","Προετοιμασία","Μέτρηση","Διαγραφή","Οριστικοποίηση","Ολοκληρώθηκε","Απέτυχε","Ακυρώθηκε","Αντιγραφή","Μετακίνηση","Διαγραφή","Αποσυσκευασία","Δεν υπάρχουν ενεργές λειτουργίες","Υπολογισμός…","Ακύρωση…","{operation} {title} ολοκληρώθηκε","{operation} ολοκληρώθηκε","{operation} {title} ακυρώθηκε","{operation} ακυρώθηκε","σφάλμα {status}","{operation} {title}: {error}","Μη διαθέσιμο","Ζητείται ακύρωση για εργασία {id}","Δεν βρέθηκε χώρος αποθήκευσης","Δωρεάν {size}","από {size}","Χρησιμοποιημένος χώρος στο {path}","PS5 συνδεδεμένο","Καμία σύνδεση","Σφάλμα API: {error}","Η λίστα είναι άδεια","Αφαίρεση","Η πηγή προστέθηκε στο manual.lst","Η πηγή είναι ήδη στο manual.lst","Κατάργηση αυτής της πηγής από το manual.lst;\n\n{path}","Η πηγή καταργήθηκε από το manual.lst","Σάρωση σε ουρά με επαναφορά των μετρητών προσπάθειας","Η σάρωση βρίσκεται στην ουρά","Η σάρωση ξεκίνησε","Τοποθετήθηκε {title}{mode}","Αποπροσαρτήθηκε {title}","Πηγή","ΔιαδροLine truncated
@@ -942,7 +942,8 @@
translations["fr-CA"] = translations["fr-FR"];
const byteUnits = {
"en-US": ["B", "KiB", "MiB", "GiB", "TiB"],
"ru-RU": ["Б", "КиБ", "МиБ", "ГиБ", "ТиБ"]
"ru-RU": ["Б", "КиБ", "МиБ", "ГиБ", "ТиБ"],
"ar-SA": ["بايت", "كيلوبايت", "ميغابايت", "غيغابايت", "تيرابايت"]
};
const supportedLanguages = Object.keys(translations);
Object.assign(translations["en-US"], {